diff --git a/core/src/output/fmt.rs b/core/src/output/fmt.rs index 40764f9..0691478 100644 --- a/core/src/output/fmt.rs +++ b/core/src/output/fmt.rs @@ -116,6 +116,14 @@ impl<'a> Span<'a> { pub fn link(text: impl Into>) -> Span<'a> { Span::new(text, FmtToken::Link) } + + pub fn is_ws(&self) -> bool { + if let Span::Content { text, .. } = self { + text.ends_with(" ") + } else { + false + } + } } impl<'a> fmt::Debug for Span<'a> { @@ -178,9 +186,34 @@ pub enum FmtToken { Link, } +pub(crate) fn write_spans_string(out: &mut String, spans: &[Span]) { + for span in spans { + match span { + Span::Content { text, .. } => out.push_str(text), + Span::Child(child) => write_spans_string(out, &child.to_spans()), + } + } +} + /// Allows an object to be converted into a token tree. pub trait TokenFmt<'a> { fn to_spans(&'a self) -> Vec>; + + fn spans_to_string(&'a self) -> String { + let mut string = String::new(); + write_spans_string(&mut string, &self.to_spans()); + string + } + + fn display(&'a self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + for span in self.to_spans() { + match span { + Span::Content { text, .. } => write!(fmt, "{}", text)?, + Span::Child(child) => child.display(fmt)?, + } + } + Ok(()) + } } pub(crate) struct JoinIter<'a, I> diff --git a/core/src/output/number_parts.rs b/core/src/output/number_parts.rs index 06c9b42..6b654a0 100644 --- a/core/src/output/number_parts.rs +++ b/core/src/output/number_parts.rs @@ -58,156 +58,10 @@ impl NumberParts { /// /// Whitespace is compacted. Any unrecognized characters are passed through. pub fn format(&self, pat: &str) -> String { - use std::io::Write; - - let mut out = vec![]; - - let mut in_ws = true; - for c in pat.chars() { - match c { - 'e' => { - if let Some(ex) = self.exact_value.as_ref() { - write!(out, "{}", ex).unwrap(); - } else { - continue; - } - } - 'a' => { - if let Some(ap) = self.approx_value.as_ref() { - write!(out, "{}", ap).unwrap(); - } else { - continue; - } - } - 'n' => match (self.exact_value.as_ref(), self.approx_value.as_ref()) { - (Some(ex), Some(ap)) => write!(out, "{}, approx. {}", ex, ap).unwrap(), - (Some(ex), None) => write!(out, "{}", ex).unwrap(), - (None, Some(ap)) => write!(out, "approx. {}", ap).unwrap(), - (None, None) => continue, - }, - 'u' => { - if let Some(unit) = self.raw_unit.as_ref() { - if unit.is_dimensionless() { - continue; - } - let mut frac = vec![]; - let mut toks = vec![]; - - if let Some(f) = self.factor.as_ref() { - toks.push("*".to_string()); - toks.push(f.to_string()); - } - for (dim, &exp) in unit.iter() { - if exp < 0 { - frac.push((dim, exp)); - } else if exp == 1 { - toks.push(dim.to_string()) - } else { - toks.push(format!("{}^{}", dim, exp)) - } - } - if !frac.is_empty() { - toks.push("/".to_string()); - if let Some(d) = self.divfactor.as_ref() { - toks.push(d.to_string()); - } - for (dim, exp) in frac { - let exp = -exp; - if exp == 1 { - toks.push(dim.to_string()) - } else { - toks.push(format!("{}^{}", dim, exp)) - } - } - } - write!(out, "{}", toks.join(" ")).unwrap(); - } else if let Some(unit) = self.unit.as_ref() { - if unit.is_empty() { - continue; - } - if let Some(f) = self.factor.as_ref() { - write!(out, "* {} ", f).unwrap(); - } - if let Some(d) = self.divfactor.as_ref() { - write!(out, "| {} ", d).unwrap(); - } - write!(out, "{}", unit).unwrap(); - } else if let Some(dim) = self.dimensions.as_ref() { - if dim.is_empty() { - continue; - } - if let Some(f) = self.factor.as_ref() { - write!(out, "* {} ", f).unwrap(); - } - if let Some(d) = self.divfactor.as_ref() { - write!(out, "| {} ", d).unwrap(); - } - write!(out, "{}", dim).unwrap(); - } else { - continue; - } - } - 'q' => { - if let Some(q) = self.quantity.as_ref() { - write!(out, "{}", q).unwrap(); - } else { - continue; - } - } - 'w' => { - if let Some(q) = self.quantity.as_ref() { - write!(out, "({})", q).unwrap(); - } else { - continue; - } - } - 'd' => { - if let Some(dim) = self.dimensions.as_ref() { - if self.unit.is_none() || dim.is_empty() { - continue; - } - write!(out, "{}", dim).unwrap(); - } else { - continue; - } - } - 'D' => { - if let Some(dim) = self.dimensions.as_ref() { - if dim.is_empty() { - continue; - } - write!(out, "{}", dim).unwrap(); - } else { - continue; - } - } - 'p' => match ( - self.quantity.as_ref(), - self.dimensions.as_ref().and_then(|x| { - if self.unit.is_some() && !x.is_empty() { - Some(x) - } else { - None - } - }), - ) { - (Some(q), Some(d)) => write!(out, "({}; {})", q, d).unwrap(), - (Some(q), None) => write!(out, "({})", q).unwrap(), - (None, Some(d)) => write!(out, "({})", d).unwrap(), - (None, None) => continue, - }, - ' ' if in_ws => continue, - ' ' if !in_ws => { - in_ws = true; - write!(out, " ").unwrap(); - continue; - } - x => write!(out, "{}", x).unwrap(), - } - in_ws = false; - } - - ::std::str::from_utf8(&out[..]).unwrap().trim().to_owned() + let spans = self.token_format(pat).to_spans(); + let mut out = String::new(); + crate::output::fmt::write_spans_string(&mut out, &spans); + out } /// A DSL for formatting numbers. @@ -233,85 +87,6 @@ impl NumberParts { } } -enum PatternToken<'a> { - Exact, // e - Approx, // a - Numeric, // n - Unit, // u - Quantity, // q - QuantityParen, // w - Dimensions, // d - DimensionsNonEmpty, // D - QuantityAndDimension, // p - Whitespace, - Passthrough(&'a str), -} - -struct FnIter(F); - -impl Iterator for FnIter -where - F: FnMut() -> Option, -{ - type Item = R; - - fn next(&mut self) -> Option { - (self.0)() - } -} - -fn is_whitespace(ch: u8) -> bool { - match ch { - b' ' | b'\n' | b'\t' => true, - _ => false, - } -} - -fn is_pattern_char(ch: u8) -> bool { - match ch { - b'e' | b'a' | b'n' | b'u' | b'q' | b'w' | b'd' | b'D' | b'p' => true, - ch if is_whitespace(ch) => true, - _ => false, - } -} - -fn parse_pattern<'a>(input: &'a str) -> impl Iterator> { - let mut i = 0; - let orig = input; - let input = input.as_bytes(); - FnIter(move || { - if i >= input.len() { - return None; - } - let tok = match input[i] { - b'e' => PatternToken::Exact, - b'a' => PatternToken::Approx, - b'n' => PatternToken::Numeric, - b'u' => PatternToken::Unit, - b'q' => PatternToken::Quantity, - b'w' => PatternToken::QuantityParen, - b'd' => PatternToken::Dimensions, - b'D' => PatternToken::DimensionsNonEmpty, - b'p' => PatternToken::QuantityAndDimension, - ch if is_whitespace(ch) => { - while i <= input.len() && is_whitespace(input[i]) { - i += 1; - } - return Some(PatternToken::Whitespace); - } - _ => { - let start = i; - while i <= input.len() && is_pattern_char(input[i]) { - i += 1; - } - return Some(PatternToken::Passthrough(&orig[start..i])); - } - }; - i += 1; - Some(tok) - }) -} - impl<'a> NumberPartsFmt<'a> { /// Converts the fmt to a span tree. Note that this is not an impl /// of TokenFmt, as it usually won't live long enough. @@ -320,37 +95,34 @@ impl<'a> NumberPartsFmt<'a> { let parts = self.number; - let mut last_was_ws = true; - for tok in parse_pattern(self.pattern) { - match tok { - PatternToken::Exact => { + for ch in self.pattern.chars() { + match ch { + 'e' => { if let Some(ref value) = parts.exact_value { tokens.push(Span::number(value)); } } - PatternToken::Approx => { + 'a' => { if let Some(ref value) = parts.approx_value { tokens.push(Span::number(value)); } } - PatternToken::Numeric => { - match (parts.exact_value.as_ref(), parts.approx_value.as_ref()) { - (Some(ex), Some(ap)) => { - tokens.push(Span::number(ex)); - tokens.push(Span::plain(", approx. ")); - tokens.push(Span::number(ap)); - } - (Some(ex), None) => { - tokens.push(Span::number(ex)); - } - (None, Some(ap)) => { - tokens.push(Span::plain("approx. ")); - tokens.push(Span::number(ap)); - } - (None, None) => (), + 'n' => match (parts.exact_value.as_ref(), parts.approx_value.as_ref()) { + (Some(ex), Some(ap)) => { + tokens.push(Span::number(ex)); + tokens.push(Span::plain(", approx. ")); + tokens.push(Span::number(ap)); } - } - PatternToken::Unit => { + (Some(ex), None) => { + tokens.push(Span::number(ex)); + } + (None, Some(ap)) => { + tokens.push(Span::plain("approx. ")); + tokens.push(Span::number(ap)); + } + (None, None) => (), + }, + 'u' => { if let Some(ref unit) = parts.raw_unit { if unit.is_dimensionless() { continue; @@ -361,7 +133,6 @@ impl<'a> NumberPartsFmt<'a> { tokens.push(Span::plain("* ")); tokens.push(Span::number(f)); tokens.push(Span::plain(" ")); - last_was_ws = true; } let mut first = true; for (dim, &exp) in unit.iter() { @@ -377,14 +148,13 @@ impl<'a> NumberPartsFmt<'a> { if exp != 1 { tokens.push(Span::pow(format!("^{}", exp))); } - last_was_ws = false; } } if !frac.is_empty() || parts.divfactor.is_some() { - if last_was_ws { - tokens.push(Span::plain("/")); - } else { + if tokens.last().map(|s| s.is_ws()) != Some(true) { tokens.push(Span::plain(" /")); + } else { + tokens.push(Span::plain("/")); } if let Some(ref d) = parts.divfactor { tokens.push(Span::plain(" ")); @@ -427,33 +197,33 @@ impl<'a> NumberPartsFmt<'a> { tokens.push(Span::unit(dim)); } } - PatternToken::Quantity => { + 'q' => { if let Some(ref quantity) = parts.quantity { tokens.push(Span::quantity(quantity)); } } - PatternToken::QuantityParen => { + 'w' => { if let Some(ref quantity) = parts.quantity { tokens.push(Span::plain("(")); tokens.push(Span::quantity(quantity)); tokens.push(Span::plain(")")); } } - PatternToken::Dimensions => { + 'd' => { if let Some(ref dim) = parts.dimensions { if parts.unit.is_some() || !dim.is_empty() { tokens.push(Span::unit(dim)); } } } - PatternToken::DimensionsNonEmpty => { + 'D' => { if let Some(ref dim) = parts.dimensions { if !dim.is_empty() { tokens.push(Span::unit(dim)); } } } - PatternToken::QuantityAndDimension => match ( + 'p' => match ( parts.quantity.as_ref(), parts.dimensions.as_ref().and_then(|dim| { if parts.unit.is_some() && !dim.is_empty() { @@ -482,16 +252,16 @@ impl<'a> NumberPartsFmt<'a> { } (None, None) => (), }, - PatternToken::Whitespace => { - tokens.push(Span::plain(" ")); - last_was_ws = true; - continue; + ' ' => { + if tokens.last().map(|s| s.is_ws()) != Some(true) { + tokens.push(Span::plain(" ")); + } } - PatternToken::Passthrough(text) => { - tokens.push(Span::plain(text)); + _ => { + // Very inefficient, but this functionality is not used anywhere in rink_core. + tokens.push(Span::plain(String::from(ch))) } } - last_was_ws = false; } // Remove trailing whitespace loop { diff --git a/core/src/output/reply.rs b/core/src/output/reply.rs index ce3436b..7628a9f 100644 --- a/core/src/output/reply.rs +++ b/core/src/output/reply.rs @@ -296,44 +296,25 @@ impl From for QueryError { impl Display for QueryReply { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - match *self { - QueryReply::Number(ref v) => write!(fmt, "{}", v), - QueryReply::Date(ref v) => write!(fmt, "{}", v), - QueryReply::Substance(ref v) => write!(fmt, "{}", v), - QueryReply::Duration(ref v) => write!(fmt, "{}", v), - QueryReply::Def(ref v) => write!(fmt, "{}", v), - QueryReply::Conversion(ref v) => write!(fmt, "{}", v), - QueryReply::Factorize(ref v) => write!(fmt, "{}", v), - QueryReply::UnitsFor(ref v) => write!(fmt, "{}", v), - QueryReply::UnitList(ref v) => write!(fmt, "{}", v), - QueryReply::Search(ref v) => write!(fmt, "{}", v), - } + self.display(fmt) } } impl Display for QueryError { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - match *self { - QueryError::Generic { ref message } => write!(fmt, "{}", message), - QueryError::Conformance(ref v) => write!(fmt, "{}", v), - QueryError::NotFound(ref v) => write!(fmt, "{}", v), - } + self.display(fmt) } } impl Display for NotFoundError { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - match self.suggestion.as_ref() { - Some(ref s) => write!(fmt, "No such unit {}, did you mean {}?", self.got, s), - None => write!(fmt, "No such unit {}", self.got), - } + self.display(fmt) } } impl Display for ConformanceError { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - writeln!(fmt, "Conformance error: {} != {}", self.left, self.right)?; - write!(fmt, "Suggestions: {}", self.suggestions.join(", ")) + self.display(fmt) } } @@ -361,165 +342,55 @@ impl DateReply { impl Display for DateReply { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - write!(fmt, "{}", self.string)?; - if let Some(ref human) = self.human { - write!(fmt, " ({})", human)?; - } - Ok(()) + self.display(fmt) } } impl Display for SubstanceReply { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - write!( - fmt, - "{}: {}{}", - self.name, - self.doc - .as_ref() - .map(|x| format!("{} ", x)) - .unwrap_or_default(), - self.properties - .iter() - .map(|prop| format!( - "{} = {}{}", - prop.name, - prop.value.format("n u"), - prop.doc - .as_ref() - .map(|x| format!(" ({})", x)) - .unwrap_or_else(|| "".to_owned()) - )) - .collect::>() - .join("; ") - ) + self.display(fmt) } } impl Display for DefReply { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - write!(fmt, "Definition: {}", self.canon_name)?; - if let Some(ref def) = self.def { - write!(fmt, " = {}", def)?; - } - if let Some(ref value) = self.value { - write!(fmt, " = {}", value.format("n u p"))?; - } - if let Some(ref doc) = self.doc { - write!(fmt, ". {}", doc)?; - } - Ok(()) + self.display(fmt) } } impl Display for ConversionReply { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - write!(fmt, "{}", self.value) + self.display(fmt) } } impl Display for FactorizeReply { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - write!( - fmt, - "Factorizations: {}", - self.factorizations - .iter() - .map(|x| { - x.units - .iter() - .map(|(u, p)| { - if *p == 1 { - u.to_string() - } else { - format!("{}^{}", u, p) - } - }) - .collect::>() - .join(" ") - }) - .collect::>() - .join("; ") - ) + self.display(fmt) } } impl Display for UnitsForReply { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - write!( - fmt, - "Units for {}: {}", - self.of.format("D w"), - self.units - .iter() - .map(|cat| { - if let Some(ref category) = cat.category { - format!("{}: {}", category, cat.units.join(", ")) - } else { - cat.units.join(", ") - } - }) - .collect::>() - .join("; ") - ) + self.display(fmt) } } impl Display for DurationReply { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - let res = [ - &self.years, - &self.months, - &self.weeks, - &self.days, - &self.hours, - &self.minutes, - ] - .iter() - .filter(|x| x.exact_value.as_ref().map(|x| &**x) != Some("0")) - .chain(once(&&self.seconds)) - .map(|x| x.to_string()) - .collect::>() - .join(", "); - write!(fmt, "{}", res)?; - if let Some(q) = self.raw.quantity.as_ref() { - write!(fmt, " ({})", q) - } else { - Ok(()) - } + self.display(fmt) } } impl Display for UnitListReply { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - write!( - fmt, - "{}", - self.list - .iter() - .map(|x| x.to_string()) - .collect::>() - .join(", ") - )?; - if let Some(q) = self.rest.quantity.as_ref() { - write!(fmt, " ({})", q) - } else { - Ok(()) - } + self.display(fmt) } } impl Display for SearchReply { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { - write!( - fmt, - "Search results: {}", - self.results - .iter() - .map(|x| x.format("u p")) - .collect::>() - .join(", ") - ) + self.display(fmt) } } diff --git a/core/tests/query.rs b/core/tests/query.rs index dc7a898..af8fb70 100644 --- a/core/tests/query.rs +++ b/core/tests/query.rs @@ -137,9 +137,9 @@ fn test_units_for() { fn test_factorize() { test( "factorize velocity", - "Factorizations: velocity; acceleration time; \ - flow_rate fuel_efficiency; \ - frequency length; jerk time^2", + "Factorizations: velocity; acceleration time; \ + flow_rate fuel_efficiency; \ + frequency length; jerk time^2", ); } @@ -249,12 +249,12 @@ fn test_convert_from_substances() { test("volume of g water", "1000 millimeter^3 (volume)"); test( "ml water -> g", - "water: volume = 1000 millimeter^3; mass = 1 gram", + "water: volume = 1000 millimeter^3 (volume); mass = 1 gram (mass)", ); test( "g water -> ml", - "water: mass = 1 gram; \ - volume = 1 milliliter", + "water: mass = 1 gram (mass); \ + volume = 1 milliliter (volume)", ); } @@ -263,10 +263,10 @@ fn test_convert_to_substances() { test( "kg -> egg", "egg: USA large egg. \ - mass = 1 kilogram; \ - egg_shelled = 20 egg; \ - egg_white = 33.[3]... egg; \ - egg_yolk = 5000/93, approx. 53.76344 egg", + mass = 1 kilogram (mass); \ + egg_shelled = 20 egg (dimensionless); \ + egg_white = 33.[3]... egg (dimensionless); \ + egg_yolk = 5000/93, approx. 53.76344 egg (dimensionless)", ); } @@ -275,7 +275,7 @@ fn test_substance_add() { test( "air", "air: Average molecular weight of air. \ - molar_mass = approx. 28.96790 gram / mole", + molar_mass = approx. 28.96790 gram / mole (molar_mass)", ); } @@ -664,17 +664,20 @@ fn test_try_decode_fail() { fn test_formula() { test( "methane=CH4", - "CH4: molar_mass = 0.01604276 kilogram / mole", + "CH4: molar_mass = 0.01604276 kilogram / mole (molar_mass)", ); test( "NaCl", - "NaCl: molar_mass = approx. 0.05844246 kilogram / mole", + "NaCl: molar_mass = approx. 0.05844246 kilogram / mole (molar_mass)", ); test( "C8H10N4O2", - "C8H10N4O2: molar_mass = approx. 0.1941931 kilogram / mole", + "C8H10N4O2: molar_mass = approx. 0.1941931 kilogram / mole (molar_mass)", + ); + test( + "C60", + "C60: molar_mass = 0.72066 kilogram / mole (molar_mass)", ); - test("C60", "C60: molar_mass = 0.72066 kilogram / mole"); } #[test] @@ -718,7 +721,7 @@ fn test_large_floats() { fn test_atom_symbol() { test( "Og", - "oganesson: atomic_number = 118; molar_mass = approx. 294.2139 gram / mole", + "oganesson: atomic_number = 118 (dimensionless); molar_mass = approx. 294.2139 gram / mole (molar_mass)", ); } diff --git a/core/tests/token_fmt.rs b/core/tests/token_fmt.rs index 0564cb9..effbf81 100644 --- a/core/tests/token_fmt.rs +++ b/core/tests/token_fmt.rs @@ -72,6 +72,20 @@ fn test(input: &str, output: &[FlatSpan<'static>]) { }); } +#[test] +fn test_dimensionless() { + test( + "42", + &[ + s("42", Number), + s(" ", Plain), + s("(", Plain), + s("dimensionless", Quantity), + s(")", Plain), + ], + ); +} + #[test] fn test_number() { test(