use smallvec::SmallVec;
use crate::{
error::Recover,
error::{label, SourceDiag},
lexer::T,
located::Located,
quantity::{Number, Value},
span::Span,
Extensions,
};
use super::{error, model::*, mt, token_stream::Token, tokens_span, warning, BlockParser};
pub struct ParsedQuantity<'a> {
pub quantity: Located<Quantity<'a>>,
pub unit_separator: Option<Span>,
}
pub(crate) fn parse_quantity<'i>(
bp: &mut BlockParser<'_, 'i>,
tokens: &[Token],
) -> ParsedQuantity<'i> {
assert!(!tokens.is_empty(), "empty quantity tokens. this is a bug.");
let mut bp2 = BlockParser::new(tokens, bp.input, bp.events, bp.extensions);
let advanced = bp2
.extension(Extensions::ADVANCED_UNITS)
.then(|| bp2.with_recover(parse_advanced_quantity))
.flatten();
advanced.unwrap_or_else(|| parse_regular_quantity(&mut bp2))
}
fn parse_regular_quantity<'i>(bp: &mut BlockParser<'_, 'i>) -> ParsedQuantity<'i> {
let mut value = value(bp);
let unit = match bp.peek() {
T![%] => {
let sep = bp.bump_any();
let unit = bp.consume_rest();
Some((sep.span, bp.text(sep.span.end(), unit)))
}
T![eof] => None,
_ => {
bp.consume_while(|t| t != T![%]);
let text = bp.text(bp.span().start(), bp.parsed());
let text_val = Value::Text(text.text_trimmed().into_owned());
value = QuantityValue {
value: Located::new(text_val, text.span()),
scaling_lock: None,
};
if let Some(sep) = bp.consume(T![%]) {
let unit = bp.consume_rest();
Some((sep.span, bp.text(sep.span.end(), unit)))
} else {
None
}
}
};
let (unit_separator, mut unit) = unit.unzip();
if let Some(unit_text) = &unit {
if unit_text.is_text_empty() {
bp.warn(
warning!(
"Empty quantity unit",
label!(unit_separator.unwrap(), "remove this")
)
.hint("Add a unit or remove the separator"),
);
unit = None;
}
}
ParsedQuantity {
quantity: Located::new(Quantity { value, unit }, tokens_span(bp.tokens())),
unit_separator,
}
}
fn parse_advanced_quantity<'i>(bp: &mut BlockParser<'_, 'i>) -> Option<ParsedQuantity<'i>> {
if bp.tokens().iter().any(|t| matches!(t.kind, T![%])) {
return None;
}
let scaling_lock = scaling_lock(bp);
bp.ws_comments();
let value_tokens = bp.consume_while(|t| !matches!(t, T![word]));
if value_tokens.is_empty() || value_tokens.last().unwrap().kind != T![ws] {
return None;
}
let value_tokens = {
let end_pos = value_tokens
.iter()
.rposition(|t| !matches!(t.kind, T![ws] | T![block comment]))
.unwrap(); &value_tokens[..=end_pos]
};
let unit_tokens = bp.consume_rest();
if unit_tokens.is_empty() {
return None;
}
let value_span = {
let start = value_tokens.first().unwrap().span.start();
let end = value_tokens.last().unwrap().span.end();
Span::new(start, end)
};
let result = range_value(value_tokens, bp).or_else(|| numeric_value(value_tokens, bp))?;
let value = match result {
Ok(value) => value,
Err(err) => {
bp.error(err);
Value::recover()
}
};
let value = Located::new(value, value_span);
let unit = bp.text(unit_tokens.first().unwrap().span.start(), unit_tokens);
Some(ParsedQuantity {
quantity: Located::new(
Quantity {
value: QuantityValue {
value,
scaling_lock,
},
unit: Some(unit),
},
tokens_span(bp.tokens()),
),
unit_separator: None,
})
}
fn value(bp: &mut BlockParser) -> QuantityValue {
let scaling_lock = scaling_lock(bp);
let value_tokens = bp.consume_while(|t| !matches!(t, T![%]));
let value = parse_value(value_tokens, bp);
QuantityValue {
value,
scaling_lock,
}
}
fn scaling_lock(bp: &mut BlockParser) -> Option<Span> {
bp.ws_comments();
match bp.peek() {
T![=] => {
let tok = bp.bump_any();
Some(tok.span)
}
_ => None,
}
}
fn parse_value(tokens: &[Token], bp: &mut BlockParser) -> Located<Value> {
let start = tokens
.first()
.map(|t| t.span.start())
.unwrap_or(bp.current_offset()); let end = bp.current_offset();
let span = Span::new(start, end);
let result = range_value(tokens, bp)
.or_else(|| numeric_value(tokens, bp))
.unwrap_or_else(|| Ok(text_value(tokens, start, bp)));
let val = match result {
Ok(value) => value,
Err(err) => {
bp.error(err);
Value::recover()
}
};
Located::new(val, span)
}
fn text_value(tokens: &[Token], offset: usize, bp: &mut BlockParser) -> Value {
let text = bp.text(offset, tokens);
if text.is_text_empty() {
bp.error(error!(
"Empty quantity value",
label!(text.span(), "add value here"),
));
}
Value::Text(text.text_trimmed().into_owned())
}
fn range_value(tokens: &[Token], bp: &BlockParser) -> Option<Result<Value, SourceDiag>> {
if !bp.extension(Extensions::RANGE_VALUES) {
return None;
}
let mid = tokens.iter().position(|t| t.kind == T![-])?;
let (start, end) = tokens.split_at(mid);
let (_mid, end) = end.split_first().unwrap();
macro_rules! unwrap_numeric {
($r:expr) => {
match $r {
Ok(Value::Number(value)) => value,
Err(err) => return Some(Err(err)),
_ => unreachable!("numeric_value not number"),
}
};
}
let start = unwrap_numeric!(numeric_value(start, bp)?);
let end = unwrap_numeric!(numeric_value(end, bp)?);
Some(Ok(Value::Range { start, end }))
}
fn not_ws_comment(t: &Token) -> bool {
!matches!(t.kind, T![ws] | T![line comment] | T![block comment])
}
fn trim_tokens(s: &[Token]) -> &[Token] {
let from = match s.iter().position(not_ws_comment) {
Some(i) => i,
None => return &s[0..0],
};
let to = s.iter().rposition(not_ws_comment).unwrap();
&s[from..=to]
}
fn numeric_value(tokens: &[Token], bp: &BlockParser) -> Option<Result<Value, SourceDiag>> {
let trimmed_tokens = trim_tokens(tokens);
if trimmed_tokens.is_empty() {
return None;
}
let r = match trimmed_tokens {
&[mt![int]] => Some(float(trimmed_tokens, bp)),
&[mt![int], mt![.], mt![int | zeroint]] | &[mt![.], mt![int | zeroint]] => {
Some(float(trimmed_tokens, bp))
}
_ => None,
};
if r.is_some() {
return r.map(|r| r.map(Value::from));
}
let filtered_tokens: SmallVec<[Token; 4]> = trimmed_tokens
.iter()
.copied()
.filter(not_ws_comment)
.collect();
let r = match *filtered_tokens.as_slice() {
[i @ mt![int], a @ mt![int], mt![/], b @ mt![int]] => mixed_num(i, a, b, bp),
[a @ mt![int], mt![/], b @ mt![int]] => frac(a, b, bp),
_ => return None,
};
Some(r.map(Value::Number))
}
fn mixed_num(i: Token, a: Token, b: Token, bp: &BlockParser) -> Result<Number, SourceDiag> {
let i = int(i, bp)?;
let Number::Fraction { num, den, .. } = frac(a, b, bp)? else {
unreachable!()
};
Ok(Number::Fraction {
whole: i,
num,
den,
err: 0.0,
})
}
fn frac(a: Token, b: Token, line: &BlockParser) -> Result<Number, SourceDiag> {
let span = Span::new(a.span.start(), b.span.end());
let a = int(a, line)?;
let b = int(b, line)?;
if b == 0 {
Err(error!("Division by zero", label!(span))
.hint("Change this please, we don't want an infinite amount of anything"))
} else {
Ok(Number::Fraction {
whole: 0,
num: a,
den: b,
err: 0.0,
})
}
}
fn int(tok: Token, block: &BlockParser) -> Result<u32, SourceDiag> {
assert_eq!(tok.kind, T![int]);
block
.token_str(tok)
.parse()
.map_err(|e| error!("Error parsing integer number", label!(tok.span)).set_source(e))
}
fn float(tokens: &[Token], bp: &BlockParser) -> Result<f64, SourceDiag> {
bp.slice_str(tokens).parse::<f64>().map_err(|e| {
error!("Error parsing decimal number", label!(tokens_span(tokens))).set_source(e)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::TokenStream;
use test_case::test_case;
macro_rules! t {
($input:expr) => {
t!($input, $crate::Extensions::all())
};
($input:expr, $extensions:expr) => {{
let input = $input;
let tokens = TokenStream::new(input).collect::<Vec<_>>();
let mut events = std::collections::VecDeque::new();
let mut bp = BlockParser::new(&tokens, input, &mut events, $extensions);
let q = parse_quantity(&mut bp, &tokens);
bp.consume_rest();
bp.finish();
let mut ctx = $crate::error::SourceReport::empty();
events.into_iter().for_each(|ev| match ev {
$crate::parser::Event::Error(e) | $crate::parser::Event::Warning(e) => ctx.push(e),
_ => {}
});
(q.quantity.into_inner(), q.unit_separator, ctx)
}};
}
macro_rules! num {
($value:expr) => {
Value::Number(Number::Regular($value))
};
}
macro_rules! range {
($start:expr, $end:expr) => {
Value::Range {
start: Number::Regular($start),
end: Number::Regular($end),
}
};
}
#[test]
fn basic_quantity() {
let (q, s, _) = t!("100%ml");
assert_eq!(
q.value,
QuantityValue {
value: Located::new(num!(100.0), 0..3),
scaling_lock: None,
}
);
assert_eq!(s, Some(Span::new(3, 4)));
assert_eq!(q.unit.unwrap().text(), "ml");
}
#[test]
fn fixed_quantity() {
let (q, s, _) = t!("=100%ml");
assert_eq!(
q.value,
QuantityValue {
value: Located::new(num!(100.0), 1..4),
scaling_lock: Some(Span::new(0, 1)),
}
);
assert_eq!(s, Some(Span::new(4, 5)));
assert_eq!(q.unit.unwrap().text(), "ml");
}
#[test]
fn fixed_quantity_with_spaces() {
let (q, s, _) = t!(" = 100%ml");
assert_eq!(
q.value,
QuantityValue {
value: Located::new(num!(100.0), 2..6),
scaling_lock: Some(Span::new(1, 2)),
}
);
assert_eq!(s, Some(Span::new(6, 7)));
assert_eq!(q.unit.unwrap().text(), "ml");
}
#[test]
fn no_separator_ext() {
let (q, s, ctx) = t!("100 ml");
assert_eq!(
q.value,
QuantityValue {
value: Located::new(num!(100.0), 0..3),
scaling_lock: None,
}
);
assert_eq!(s, None);
assert_eq!(q.unit.unwrap().text(), "ml");
assert!(ctx.is_empty());
let (q, s, ctx) = t!("100 ml", Extensions::all() ^ Extensions::ADVANCED_UNITS);
assert_eq!(
q.value,
QuantityValue {
value: Located::new(Value::Text("100 ml".into()), 0..6),
scaling_lock: None,
}
);
assert_eq!(s, None);
assert_eq!(q.unit, None);
assert!(ctx.is_empty());
}
#[test]
fn no_separator_range() {
let (q, s, ctx) = t!("100-200 ml");
assert_eq!(
q.value,
QuantityValue {
value: Located::new(range!(100.0, 200.0), 0..7),
scaling_lock: None,
}
);
assert_eq!(s, None);
assert_eq!(q.unit.unwrap().text(), "ml");
assert!(ctx.is_empty());
let (q, s, ctx) = t!("1 - 2 1 / 2 ml");
assert_eq!(
q.value,
QuantityValue {
value: Located::new(
Value::Range {
start: 1.0.into(),
end: Number::Fraction {
whole: 2,
num: 1,
den: 2,
err: 0.0
}
},
0..11
),
scaling_lock: None,
}
);
assert_eq!(s, None);
assert_eq!(q.unit.unwrap().text(), "ml");
assert!(ctx.is_empty());
}
#[test]
fn range_value() {
let (q, _, _) = t!("2-3");
assert_eq!(
q.value,
QuantityValue {
value: Located::new(range!(2.0, 3.0), 0..3),
scaling_lock: None,
}
);
assert_eq!(q.unit, None);
}
#[test]
fn range_value_no_extension() {
let (q, _, _) = t!("2-3", Extensions::empty());
assert_eq!(
q.value,
QuantityValue {
value: Located::new(Value::Text("2-3".into()), 0..3),
scaling_lock: None,
}
);
assert_eq!(q.unit, None);
}
#[test]
fn range_mixed_value() {
let (q, _, _) = t!("2 1/2-3");
assert_eq!(
q.value,
QuantityValue {
value: Located::new(range!(2.5, 3.0), 0..7),
scaling_lock: None,
}
);
assert_eq!(q.unit, None);
let (q, _, _) = t!("2-3 1/2");
assert_eq!(
q.value,
QuantityValue {
value: Located::new(range!(2.0, 3.5), 0..7),
scaling_lock: None,
}
);
assert_eq!(q.unit, None);
let (q, _, _) = t!("2 1/2-3 1/2");
assert_eq!(
q.value,
QuantityValue {
value: Located::new(range!(2.5, 3.5), 0..11),
scaling_lock: None,
}
);
assert_eq!(q.unit, None);
}
#[test_case("1/2" => (0, 1, 2); "fraction")]
#[test_case("0 1/2" => (0, 1, 2); "zero whole")]
#[test_case("01/2" => panics "not number"; "bad fraction")]
#[test_case("2 1/2" => (2, 1, 2); "mixed value")]
fn fractional_val(s: &str) -> (u32, u32, u32) {
let (q, _, _) = t!(s);
let value = q.value.value.into_inner();
let Value::Number(num) = value else {
panic!("not number")
};
let Number::Fraction {
whole,
num,
den,
err,
} = num
else {
panic!("not fraction")
};
assert_eq!(err, 0.0);
(whole, num, den)
}
#[test_case("1" => 1.0)]
#[test_case("1.0" => 1.0)]
#[test_case("10" => 10.0)]
#[test_case("10.0000000" => 10.0)]
#[test_case("10.05" => 10.05)]
#[test_case("01" => panics "not number")]
#[test_case("01.0" => panics "not number")]
fn simple_numbers(s: &str) -> f64 {
let (q, _, r) = t!(s);
let value = q.value.value.into_inner();
let Value::Number(num) = value else {
panic!("not number")
};
let Number::Regular(n) = num else {
panic!("not regular number")
};
assert!(r.is_empty(), "source error");
n
}
}