use crate::math::FourierCoefficients;
use std::collections::BTreeSet;
use std::sync::Arc;
use super::*;
#[rustfmt::skip]
const ZERO_VALUED_IMPLICIT_GAMUT_ELEMENTS: [&str; 16] = [
"x_0", "x_1", "x_2", "x_3",
"y_0", "y_1", "y_2", "y_3",
"lat_0", "lat_1", "lat_2", "lat_3",
"lon_0", "lon_1", "lon_2", "lon_3"
];
#[rustfmt::skip]
const UNIT_VALUED_IMPLICIT_GAMUT_ELEMENTS: [&str; 4] = [
"k_0", "k_1", "k_2", "k_3"
];
#[derive(Debug, Clone)]
pub struct ParsedParameters {
pub name: String,
pub boolean: BTreeSet<&'static str>,
pub natural: BTreeMap<&'static str, usize>,
pub integer: BTreeMap<&'static str, i64>,
pub real: BTreeMap<&'static str, f64>,
pub series: BTreeMap<&'static str, Vec<f64>>,
pub text: BTreeMap<&'static str, String>,
pub texts: BTreeMap<&'static str, Vec<String>>,
pub uuid: BTreeMap<&'static str, uuid::Uuid>,
pub fourier_coefficients: BTreeMap<&'static str, FourierCoefficients>,
pub ignored: Vec<String>,
pub given: BTreeMap<String, String>,
pub grids: Vec<Arc<BaseGrid>>,
}
impl ParsedParameters {
pub fn boolean(&self, key: &str) -> bool {
self.boolean.contains(key)
}
pub fn natural(&self, key: &str) -> Result<usize, Error> {
if let Some(value) = self.natural.get(key) {
return Ok(*value);
}
Err(Error::MissingParam(key.to_string()))
}
pub fn integer(&self, key: &str) -> Result<i64, Error> {
if let Some(value) = self.integer.get(key) {
return Ok(*value);
}
Err(Error::MissingParam(key.to_string()))
}
pub fn real(&self, key: &str) -> Result<f64, Error> {
if let Some(value) = self.real.get(key) {
return Ok(*value);
}
Err(Error::MissingParam(key.to_string()))
}
pub fn series(&self, key: &str) -> Result<&[f64], Error> {
if let Some(value) = self.series.get(key) {
return Ok(value);
}
Err(Error::MissingParam(key.to_string()))
}
pub fn series_as_i64(&self, key: &str) -> Result<Vec<i64>, Error> {
let args: Vec<i64> = self
.series(key)
.unwrap()
.iter()
.map(|i| *i as i64)
.collect();
Ok(args)
}
pub fn series_as_usize(&self, key: &str) -> Result<Vec<usize>, Error> {
let args: Vec<usize> = self
.series(key)
.unwrap()
.iter()
.map(|i| *i as usize)
.collect();
Ok(args)
}
pub fn text(&self, key: &str) -> Result<String, Error> {
if let Some(value) = self.text.get(key) {
return Ok(value.to_string());
}
Err(Error::MissingParam(key.to_string()))
}
pub fn texts(&self, key: &str) -> Result<&Vec<String>, Error> {
if let Some(value) = self.texts.get(key) {
return Ok(value);
}
Err(Error::MissingParam(key.to_string()))
}
pub fn uuid(&self, key: &str) -> Result<uuid::Uuid, Error> {
if let Some(value) = self.uuid.get(key) {
return Ok(*value);
}
Err(Error::MissingParam(key.to_string()))
}
pub fn fourier_coefficients(&self, key: &str) -> Result<FourierCoefficients, Error> {
if let Some(value) = self.fourier_coefficients.get(key) {
return Ok(*value);
}
Err(Error::MissingParam(key.to_string()))
}
pub fn ignored(&self) -> Vec<String> {
self.ignored.clone()
}
pub fn ellps(&self, index: usize) -> Ellipsoid {
if index == 0 {
if let Some(e) = self.text.get("ellps") {
return Ellipsoid::named(e).unwrap();
}
}
let key = format!("ellps_{index}");
if let Some(e) = self.text.get(&key[..]) {
return Ellipsoid::named(e).unwrap();
}
Ellipsoid::default()
}
pub fn k(&self, index: usize) -> f64 {
*(self.real.get(&format!("k_{index}")[..]).unwrap_or(&1.))
}
pub fn x(&self, index: usize) -> f64 {
*(self.real.get(&format!("x_{index}")[..]).unwrap_or(&0.))
}
pub fn y(&self, index: usize) -> f64 {
*(self.real.get(&format!("y_{index}")[..]).unwrap_or(&0.))
}
pub fn lat(&self, index: usize) -> f64 {
*self.real.get(&format!("lat_{index}")[..]).unwrap_or(&0.)
}
pub fn lon(&self, index: usize) -> f64 {
*self.real.get(&format!("lon_{index}")[..]).unwrap_or(&0.)
}
}
impl ParsedParameters {
pub fn new(
parameters: &RawParameters,
gamut: &[OpParameter],
) -> Result<ParsedParameters, Error> {
let locals = parameters.instantiated_as.split_into_parameters();
let globals = ¶meters.globals;
let mut boolean = BTreeSet::<&'static str>::new();
let mut natural = BTreeMap::<&'static str, usize>::new();
let mut integer = BTreeMap::<&'static str, i64>::new();
let mut real = BTreeMap::<&'static str, f64>::new();
let mut series = BTreeMap::<&'static str, Vec<f64>>::new();
let mut text = BTreeMap::<&'static str, String>::new();
let mut texts = BTreeMap::<&'static str, Vec<String>>::new();
let grids = Vec::new();
let uuid = BTreeMap::<&'static str, uuid::Uuid>::new();
let fourier_coefficients = BTreeMap::<&'static str, FourierCoefficients>::new();
for p in gamut {
match *p {
OpParameter::Flag { key } => {
if let Some(value) = chase(globals, &locals, key)? {
if value.is_empty() || value.to_lowercase() == "true" {
boolean.insert(key);
continue;
}
warn!("Cannot parse {key}:{value} as a boolean constant!");
return Err(Error::BadParam(key.to_string(), value));
}
continue;
}
OpParameter::Natural { key, default } => {
if let Some(value) = chase(globals, &locals, key)? {
if let Ok(v) = value.parse::<usize>() {
natural.insert(key, v);
continue;
}
warn!("Cannot parse {key}:{value} as a natural number!");
return Err(Error::BadParam(key.to_string(), value));
}
if let Some(value) = default {
natural.insert(key, value);
continue;
}
error!("Missing required parameter '{key}'");
return Err(Error::MissingParam(key.to_string()));
}
OpParameter::Integer { key, default } => {
if let Some(value) = chase(globals, &locals, key)? {
if let Ok(v) = value.parse::<i64>() {
integer.insert(key, v);
continue;
}
warn!("Cannot parse {key}:{value} as an integer!");
return Err(Error::BadParam(key.to_string(), value));
}
if let Some(value) = default {
integer.insert(key, value);
continue;
}
error!("Missing required parameter '{key}'");
return Err(Error::MissingParam(key.to_string()));
}
OpParameter::Real { key, default } => {
if let Some(value) = chase(globals, &locals, key)? {
let v = angular::parse_sexagesimal(&value);
if v.is_nan() {
return Err(Error::BadParam(key.to_string(), value));
}
real.insert(key, v);
continue;
}
if let Some(value) = default {
real.insert(key, value);
continue;
}
error!("Missing required parameter '{key}'");
return Err(Error::MissingParam(key.to_string()));
}
OpParameter::Series { key, default } => {
let mut elements = Vec::<f64>::new();
if let Some(value) = chase(globals, &locals, key)? {
for element in value.split(',') {
let v = angular::parse_sexagesimal(element);
if v.is_nan() {
warn!("Cannot parse {key}:{value} as a series");
return Err(Error::BadParam(key.to_string(), value.to_string()));
}
elements.push(v);
continue;
}
series.insert(key, elements);
continue;
}
if let Some(value) = default {
if value.is_empty() {
continue;
}
for element in value.split(',') {
let v = angular::parse_sexagesimal(element);
if v.is_nan() {
warn!("Cannot parse {key}:{value} as a series");
return Err(Error::BadParam(key.to_string(), value.to_string()));
}
elements.push(v);
continue;
}
series.insert(key, elements);
continue;
}
error!("Missing required parameter '{key}'");
return Err(Error::MissingParam(key.to_string()));
}
OpParameter::Text { key, default } => {
if let Some(value) = chase(globals, &locals, key)? {
text.insert(key, value.to_string());
continue;
}
if let Some(value) = default {
text.insert(key, value.to_string());
continue;
}
error!("Missing required parameter '{key}'");
return Err(Error::MissingParam(key.to_string()));
}
OpParameter::Texts { key, default } => {
if let Some(value) = chase(globals, &locals, key)? {
let elements: Vec<String> =
value.split(',').map(|x| x.trim().to_string()).collect();
texts.insert(key, elements);
continue;
}
if let Some(value) = default {
if value.is_empty() {
continue;
}
let elements: Vec<String> =
value.split(',').map(|x| x.trim().to_string()).collect();
texts.insert(key, elements);
continue;
}
error!("Missing required parameter '{key}'");
return Err(Error::MissingParam(key.to_string()));
}
};
}
if let Some(value) = chase(globals, &locals, "omit_fwd")? {
if value.is_empty() || value.to_lowercase() == "true" {
boolean.insert("omit_fwd");
}
}
if let Some(value) = chase(globals, &locals, "omit_inv")? {
if value.is_empty() || value.to_lowercase() == "true" {
boolean.insert("omit_inv");
}
}
for k in ZERO_VALUED_IMPLICIT_GAMUT_ELEMENTS {
if !real.contains_key(k) {
real.insert(k, 0.);
}
}
for k in UNIT_VALUED_IMPLICIT_GAMUT_ELEMENTS {
if !real.contains_key(k) {
real.insert(k, 1.);
}
}
let name = locals
.get("_name")
.unwrap_or(&"unknown".to_string())
.to_string();
let given = locals.clone();
let ignored: Vec<String> = locals.into_keys().collect();
Ok(ParsedParameters {
name,
boolean,
natural,
integer,
real,
series,
grids,
text,
texts,
uuid,
fourier_coefficients,
ignored,
given,
})
}
}
pub fn chase(
globals: &BTreeMap<String, String>,
locals: &BTreeMap<String, String>,
key: &str,
) -> Result<Option<String>, Error> {
let mut haystack = globals.iter().chain(locals.iter()).rev();
let key = key.trim();
if key.is_empty() {
return Err(Error::Syntax(String::from("Empty key")));
}
let mut default = "";
let mut needle = key;
let mut chasing = false;
let value;
loop {
let found = haystack.find(|&x| x.0 == needle);
if found.is_none() {
if !default.is_empty() {
return Ok(Some(String::from(default)));
}
if chasing {
return Err(Error::Syntax(format!(
"Incomplete definition for '{key}' ('{needle}' not found)"
)));
}
return Ok(None);
}
let thevalue = found.unwrap().1.trim();
if let Some(stripped) = thevalue.strip_prefix('$') {
let mut parts: Vec<_> = stripped
.trim()
.split(&['(', ')'][..])
.filter(|x| !x.trim().is_empty())
.collect();
if ![1, 2].contains(&parts.len()) {
return Err(Error::Syntax(format!(
"Bad format for optional default in '{thevalue}'"
)));
}
if parts.len() == 2 && !chasing {
default = parts.pop().unwrap();
}
chasing = true;
needle = parts.pop().unwrap();
continue;
}
if let Some(stripped) = thevalue.strip_prefix('(') {
chasing = true;
needle = key;
default = stripped.trim_end_matches(')');
continue;
}
value = String::from(thevalue.trim());
break;
}
Ok(Some(value))
}
#[cfg(test)]
mod tests {
use super::*;
#[rustfmt::skip]
const GAMUT: [OpParameter; 11] = [
OpParameter::Flag { key: "flag" },
OpParameter::Natural { key: "natural", default: Some(0) },
OpParameter::Integer { key: "integer", default: Some(-1)},
OpParameter::Real { key: "real", default: Some(1.25) },
OpParameter::Real { key: "sexagesimal", default: Some(1.25) },
OpParameter::Series { key: "series", default: Some("1,2,3,4") },
OpParameter::Series { key: "bad_series", default: Some("1:2:3,2:3:4") },
OpParameter::Text { key: "text", default: Some("text") },
OpParameter::Texts { key: "names", default: Some("foo, bar") },
OpParameter::Texts { key: "foo", default: Some(" bar ") },
OpParameter::Text { key: "ellps_0", default: Some("6400000, 300") },
];
#[test]
fn basic() -> Result<(), Error> {
let mut globals = BTreeMap::<String, String>::new();
globals.insert("indirection".to_string(), "123".to_string());
let invocation = String::from(
"cucumber flag ellps_0=123 , 456 natural=$indirection sexagesimal=1:30:36 names=alice, bob",
);
let raw = RawParameters::new(&invocation, &globals);
let p = ParsedParameters::new(&raw, &GAMUT)?;
assert!(
p.boolean.contains("flag"),
"`flag` not in registered booleans: {:#?}",
p.boolean
);
assert!(
!p.boolean.contains("galf"),
"`galf` not in registered booleans: {:?}",
p.boolean
);
let series = p.series.get("series").unwrap();
assert_eq!(series.len(), 4);
assert_eq!(series[0], 1.);
assert_eq!(series[3], 4.);
let texts = p.texts.get("names").unwrap();
assert_eq!(texts.len(), 2);
assert_eq!(texts[0], "alice");
assert_eq!(texts[1], "bob");
let texts = p.texts.get("foo").unwrap();
assert_eq!(texts.len(), 1);
assert_eq!(texts[0], "bar");
assert_eq!(*p.real.get("sexagesimal").unwrap(), 1.51);
assert_eq!(*p.natural.get("natural").unwrap(), 123_usize);
assert_eq!(*p.integer.get("integer").unwrap(), -1);
assert_eq!(*p.text.get("text").unwrap(), "text");
assert_eq!(
p.ellps(0).semimajor_axis(),
Ellipsoid::new(123., 1. / 456.).semimajor_axis()
);
let invocation = String::from("cucumber bad_series=no, numbers, here");
let raw = RawParameters::new(&invocation, &globals);
assert!(matches!(
ParsedParameters::new(&raw, &GAMUT),
Err(Error::BadParam(_, _))
));
let invocation = String::from("cucumber integer=$not_given");
let raw = RawParameters::new(&invocation, &globals);
assert!(matches!(
ParsedParameters::new(&raw, &GAMUT),
Err(Error::Syntax(_))
));
let invocation = String::from("cucumber integer=$not_given_but_defaults_to_42(42)");
let raw = RawParameters::new(&invocation, &globals);
assert_eq!(
*ParsedParameters::new(&raw, &GAMUT)
.unwrap()
.integer
.get("integer")
.unwrap(),
42
);
globals.insert("given_and_is_set_to_43".to_string(), "43".to_string());
let invocation = String::from("cucumber integer=$given_and_is_set_to_43(42)");
let raw = RawParameters::new(&invocation, &globals);
assert_eq!(
*ParsedParameters::new(&raw, &GAMUT)
.unwrap()
.integer
.get("integer")
.unwrap(),
43
);
Ok(())
}
}