use crate::SyntaxKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CostSpecDefect {
EmptyComponent,
TrailingJunk,
}
const fn is_cost_trivia(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
| SyntaxKind::COMMENT
| SyntaxKind::PERCENT_COMMENT
| SyntaxKind::SHEBANG
| SyntaxKind::EMACS_DIRECTIVE
)
}
pub(super) fn first_cost_spec_defect<P, I>(tokens: I) -> Option<(CostSpecDefect, P)>
where
I: IntoIterator<Item = (SyntaxKind, P)>,
{
let mut saw_comma = false;
let mut component_has_content = false;
let mut component_complete = false;
let mut pending_empty = false;
for (kind, range) in tokens {
if is_cost_trivia(kind)
|| matches!(
kind,
SyntaxKind::L_BRACE | SyntaxKind::L_DOUBLE_BRACE | SyntaxKind::L_BRACE_HASH
)
{
continue;
}
match kind {
SyntaxKind::COMMA | SyntaxKind::R_BRACE | SyntaxKind::R_DOUBLE_BRACE => {
let closing = kind != SyntaxKind::COMMA;
if !closing {
saw_comma = true;
}
if !component_has_content && (saw_comma || pending_empty) {
return Some((CostSpecDefect::EmptyComponent, range));
}
if closing {
return None;
}
pending_empty = true;
component_has_content = false;
component_complete = false;
}
SyntaxKind::CURRENCY | SyntaxKind::DATE | SyntaxKind::STRING => {
if component_complete {
return Some((CostSpecDefect::TrailingJunk, range));
}
component_has_content = true;
component_complete = true;
}
_ => {
if component_complete {
return Some((CostSpecDefect::TrailingJunk, range));
}
component_has_content = true;
}
}
}
None
}
pub(super) fn cost_defect_message(defect: CostSpecDefect, text: &str) -> String {
match defect {
CostSpecDefect::EmptyComponent => format!(
"empty cost-spec component before {text:?}: \
each comma-separated component needs a number, currency, date, \
string or `*`"
),
CostSpecDefect::TrailingJunk => format!(
"unexpected {text:?} in cost spec: the component is already \
complete, so only `,` or the closing brace may follow"
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use SyntaxKind as K;
use std::ops::Range;
fn toks(items: &[(K, &str)]) -> Vec<(K, Range<usize>)> {
let mut at = 0usize;
items
.iter()
.map(|(k, t)| {
let r = at..at + t.len();
at = r.end;
(*k, r)
})
.collect()
}
const WS: (K, &str) = (K::WHITESPACE, " ");
const LB: (K, &str) = (K::L_BRACE, "{");
const RB: (K, &str) = (K::R_BRACE, "}");
#[test]
fn empty_cost_spec_is_valid() {
assert_eq!(first_cost_spec_defect(toks(&[LB, RB])), None);
assert_eq!(first_cost_spec_defect(toks(&[LB, WS, RB])), None);
}
#[test]
fn ordinary_specs_are_valid() {
assert_eq!(
first_cost_spec_defect(toks(&[
LB,
(K::NUMBER, "100.0"),
WS,
(K::CURRENCY, "USD"),
RB
])),
None
);
assert_eq!(
first_cost_spec_defect(toks(&[
LB,
(K::NUMBER, "100.0"),
WS,
(K::CURRENCY, "USD"),
(K::COMMA, ","),
WS,
(K::DATE, "2015-01-01"),
RB
])),
None
);
assert_eq!(
first_cost_spec_defect(toks(&[
LB,
(K::NUMBER, "100.0"),
WS,
(K::CURRENCY, "USD"),
(K::COMMA, ","),
(K::STRING, "\"lot\""),
(K::COMMA, ","),
(K::STAR, "*"),
RB
])),
None
);
assert_eq!(
first_cost_spec_defect(toks(&[
LB,
WS,
(K::HASH, "#"),
WS,
(K::NUMBER, "500.00"),
WS,
(K::CURRENCY, "USD"),
RB
])),
None
);
}
#[test]
fn arithmetic_inside_a_component_is_valid() {
for op in [
(K::STAR, "*"),
(K::SLASH, "/"),
(K::PLUS, "+"),
(K::MINUS, "-"),
] {
assert_eq!(
first_cost_spec_defect(toks(&[
LB,
(K::NUMBER, "10.00"),
WS,
op,
WS,
(K::NUMBER, "3"),
WS,
(K::CURRENCY, "USD"),
RB
])),
None,
"arithmetic operator {op:?} must be allowed before the currency"
);
}
}
#[test]
fn empty_components_are_rejected() {
let leading = toks(&[
LB,
(K::COMMA, ","),
WS,
(K::NUMBER, "100.0"),
WS,
(K::CURRENCY, "USD"),
RB,
]);
assert_eq!(
first_cost_spec_defect(leading.clone()).map(|(d, _)| d),
Some(CostSpecDefect::EmptyComponent)
);
assert_eq!(first_cost_spec_defect(leading).map(|(_, r)| r), Some(1..2));
assert_eq!(
first_cost_spec_defect(toks(&[
LB,
(K::NUMBER, "1"),
WS,
(K::CURRENCY, "USD"),
(K::COMMA, ","),
(K::COMMA, ","),
RB
]))
.map(|(d, _)| d),
Some(CostSpecDefect::EmptyComponent)
);
assert_eq!(
first_cost_spec_defect(toks(&[
LB,
(K::NUMBER, "1"),
WS,
(K::CURRENCY, "USD"),
(K::COMMA, ","),
WS,
RB
]))
.map(|(d, _)| d),
Some(CostSpecDefect::EmptyComponent)
);
}
#[test]
fn junk_after_a_complete_component_is_rejected() {
let stream = toks(&[
LB,
(K::NUMBER, "45.23"),
WS,
(K::CURRENCY, "USD"),
WS,
(K::SLASH, "/"),
WS,
(K::DATE, "2015-07-16"),
WS,
(K::SLASH, "/"),
WS,
(K::STRING, "\"blabla\""),
RB,
]);
let expected = stream[5].1.clone();
let (defect, range) = first_cost_spec_defect(stream).expect("junk must be reported");
assert_eq!(defect, CostSpecDefect::TrailingJunk);
assert_eq!(range, expected);
}
#[test]
fn two_currencies_in_one_component_are_rejected() {
assert_eq!(
first_cost_spec_defect(toks(&[
LB,
(K::NUMBER, "2"),
WS,
(K::CURRENCY, "AAPL"),
WS,
(K::CURRENCY, "USD"),
RB
]))
.map(|(d, _)| d),
Some(CostSpecDefect::TrailingJunk)
);
}
#[test]
fn double_brace_total_cost_is_valid() {
const LLB: (K, &str) = (K::L_DOUBLE_BRACE, "{{");
const RRB: (K, &str) = (K::R_DOUBLE_BRACE, "}}");
assert_eq!(
first_cost_spec_defect(toks(&[
LLB,
(K::NUMBER, "500.00"),
WS,
(K::CURRENCY, "USD"),
RRB
])),
None
);
assert_eq!(
first_cost_spec_defect(toks(&[
LLB,
(K::NUMBER, "500.00"),
WS,
(K::CURRENCY, "USD"),
(K::COMMA, ","),
WS,
(K::DATE, "2015-01-01"),
RRB
])),
None
);
assert_eq!(
first_cost_spec_defect(toks(&[
(K::L_BRACE_HASH, "{#"),
WS,
(K::NUMBER, "500.00"),
WS,
(K::CURRENCY, "USD"),
RB
])),
None
);
}
#[test]
fn currency_only_is_left_to_the_booker() {
assert_eq!(
first_cost_spec_defect(toks(&[LB, (K::CURRENCY, "USD"), RB])),
None
);
assert_eq!(
first_cost_spec_defect(toks(&[
LB,
WS,
(K::HASH, "#"),
WS,
(K::CURRENCY, "USD"),
RB
])),
None
);
}
}