use std::fmt;
use crate::DataType;
use crate::sid::normalize_trim;
use crate::sid_spelling::{Concat, SidSpelling};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Negatives {
Minus,
Parentheses,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NumberFormatError {
Empty,
NoDigits,
MarkAfterPoint,
TwoPoints,
GroupIsNotThree(usize),
DigitsAfterSymbol,
TooManyPictures,
NegativeIsNotBracketed,
}
impl fmt::Display for NumberFormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => f.write_str(
"the format is empty. Write how the source writes the value, with its digits \
replaced by digit places — `$#,##0.00` for `$2,899.99`, `0.000` for `1.158`",
),
Self::NoDigits => f.write_str(
"the format holds no digit place, so there is no number in it. A digit place is \
`0` or `#` — `$#,##0.00` for `$2,899.99`",
),
Self::MarkAfterPoint => f.write_str(
"the format puts a `,` after the `.`. In a picture `,` is the thousands mark and \
`.` is the decimal point, whichever characters the source itself uses, so the \
thousands come first. A source that writes `2.899,99` is not covered today",
),
Self::TwoPoints => f.write_str(
"the format has two decimal points, and a number has one. If one of them is the \
thousands mark, write it as `,` — `$#,##0.00` is two thousand eight hundred and \
ninety-nine point nine nine",
),
Self::GroupIsNotThree(places) => write!(
f,
"the format puts {places} digit {} after a `,`, and a thousands mark \
separates groups of three. Write `#,##0` for a number written `2,899`",
if *places == 1 { "place" } else { "places" }
),
Self::DigitsAfterSymbol => f.write_str(
"the format has digit places on both sides of the symbol. The symbol opens the \
value or closes it — `$#,##0.00` or `#,##0.00 kr` — and a number written in two \
halves is not covered",
),
Self::TooManyPictures => f.write_str(
"the format holds more than two pictures. One picture says how the source writes \
a value; a second, after a `;`, says how it writes a negative one",
),
Self::NegativeIsNotBracketed => f.write_str(
"the second picture is not the first one in brackets. It says the source writes a \
negative the way a ledger does, so it is the first picture inside `(` and `)` — \
`$#,##0.00;($#,##0.00)`. A source that writes a leading minus needs no second \
picture at all",
),
}
}
}
impl std::error::Error for NumberFormatError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NumberFormat {
spelled: String,
opens_with: String,
closes_with: String,
grouped: bool,
decimals: usize,
negatives: Negatives,
}
const DIGIT_PLACES: [char; 2] = ['#', '0'];
const THOUSANDS_MARK: char = ',';
const DECIMAL_POINT: char = '.';
impl NumberFormat {
pub fn parse(spelled: &str) -> Result<Self, NumberFormatError> {
if spelled.is_empty() {
return Err(NumberFormatError::Empty);
}
let mut pictures = spelled.split(';');
let positive = pictures.next().unwrap_or_default();
let negative = pictures.next();
if pictures.next().is_some() {
return Err(NumberFormatError::TooManyPictures);
}
let mut read = Self::read_one(positive)?;
if let Some(negative) = negative {
if negative != format!("({positive})") {
return Err(NumberFormatError::NegativeIsNotBracketed);
}
read.negatives = Negatives::Parentheses;
}
spelled.clone_into(&mut read.spelled);
Ok(read)
}
fn read_one(picture: &str) -> Result<Self, NumberFormatError> {
let is_body = |c: char| DIGIT_PLACES.contains(&c) || c == THOUSANDS_MARK;
let first =
picture.find(|c| DIGIT_PLACES.contains(&c)).ok_or(NumberFormatError::NoDigits)?;
let opens_with = picture[..first].to_owned();
let rest = &picture[first..];
let mut body = String::new();
let mut seen_point = false;
let mut closes_at = rest.len();
for (at, character) in rest.char_indices() {
if is_body(character) {
if seen_point && character == THOUSANDS_MARK {
return Err(NumberFormatError::MarkAfterPoint);
}
body.push(character);
continue;
}
if character == DECIMAL_POINT {
if seen_point {
return Err(NumberFormatError::TwoPoints);
}
seen_point = true;
body.push(character);
continue;
}
closes_at = at;
break;
}
let closes_with = rest[closes_at..].to_owned();
if closes_with.contains(DIGIT_PLACES) {
return Err(NumberFormatError::DigitsAfterSymbol);
}
let (whole, fraction) = match body.split_once(DECIMAL_POINT) {
Some((whole, fraction)) => (whole, fraction),
None => (body.as_str(), ""),
};
if fraction.contains(THOUSANDS_MARK) {
return Err(NumberFormatError::MarkAfterPoint);
}
let grouped = whole.contains(THOUSANDS_MARK);
if grouped {
for group in whole.split(THOUSANDS_MARK).skip(1) {
if group.chars().count() != 3 {
return Err(NumberFormatError::GroupIsNotThree(group.chars().count()));
}
}
}
Ok(Self {
spelled: picture.to_owned(),
opens_with,
closes_with,
grouped,
decimals: fraction.chars().count(),
negatives: Negatives::Minus,
})
}
#[must_use]
pub fn spelled(&self) -> &str {
&self.spelled
}
#[must_use]
pub fn opens_with(&self) -> &str {
&self.opens_with
}
#[must_use]
pub fn closes_with(&self) -> &str {
&self.closes_with
}
#[must_use]
pub fn grouped(&self) -> bool {
self.grouped
}
#[must_use]
pub fn decimals(&self) -> usize {
self.decimals
}
#[must_use]
pub fn negatives(&self) -> Negatives {
self.negatives
}
#[must_use]
pub fn marks(&self) -> Vec<String> {
let mut marks = Vec::new();
if !self.opens_with.is_empty() {
marks.push(self.opens_with.clone());
}
if !self.closes_with.is_empty() {
marks.push(self.closes_with.clone());
}
if self.grouped {
marks.push(THOUSANDS_MARK.to_string());
}
if self.negatives == Negatives::Parentheses {
marks.push("(".to_owned());
marks.push(")".to_owned());
}
marks
}
#[must_use]
pub fn shape_pattern(&self) -> String {
let digits = if self.grouped { "[0-9][0-9,]*" } else { "[0-9]+" };
let fraction = if self.decimals > 0 { "([.][0-9]+)?" } else { "" };
let (open, close) = match self.negatives {
Negatives::Minus => ("-?", ""),
Negatives::Parentheses => ("[(]?", "[)]?"),
};
format!(
"^{open}{}{digits}{fraction}{}{close}$",
optional_literal(&self.opens_with),
optional_literal(&self.closes_with),
)
}
}
impl fmt::Display for NumberFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.spelled)
}
}
fn optional_literal(text: &str) -> String {
if text.is_empty() {
return String::new();
}
let mut pattern = String::new();
for character in text.chars() {
if character.is_ascii_alphanumeric() {
pattern.push(character);
} else {
pattern.push('[');
pattern.push(character);
pattern.push(']');
}
}
format!("({pattern})?")
}
#[must_use]
pub fn is_a_number(target: &DataType) -> bool {
matches!(target, DataType::Integer(_) | DataType::Decimal(_) | DataType::Float(_))
}
#[must_use]
pub fn strip_marks(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> String {
let replace = spelling.lower.replace;
let prefix = spelling.lower.pattern_literal_prefix;
format.marks().iter().fold(normalize_trim(spelling, expr), |text, mark| {
format!("{replace}({text}, {prefix}'{}', {prefix}'')", mark.replace('\'', "''"))
})
}
fn separators_in_order(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> Option<String> {
if !format.grouped() {
return None;
}
let prefix = spelling.lower.pattern_literal_prefix;
Some(format!(
"{} NOT LIKE {prefix}'%{DECIMAL_POINT}%{THOUSANDS_MARK}%'",
normalize_trim(spelling, expr)
))
}
#[derive(Clone, Copy)]
pub enum ClassTest<'a> {
Flat(&'a dyn Fn(&str, &str) -> String),
Folded,
}
fn flat_shape_conditions(
format: &NumberFormat,
spelling: SidSpelling,
expr: &str,
holds: &dyn Fn(&str, &str) -> String,
) -> Vec<String> {
let prefix = spelling.lower.pattern_literal_prefix;
let value = normalize_trim(spelling, expr);
let stripped = strip_marks(format, spelling, expr);
let not_like = |subject: &str, pattern: &str| {
format!("{subject} NOT LIKE {prefix}'{}'", pattern.replace('\'', "''"))
};
let mut conditions = symbol_conditions(format, spelling, &value);
conditions.push(holds(&stripped, "[0-9]"));
conditions.push(format!("NOT {}", holds(&stripped, "[^0-9.-]")));
let bracketed = format.negatives() == Negatives::Parentheses;
conditions.push(not_like(&stripped, if bracketed { "%-%" } else { "_%-%" }));
if format.decimals() > 0 {
for shape in ["%.%.%", ".%", "-.%", "%."] {
conditions.push(not_like(&stripped, shape));
}
} else {
conditions.push(not_like(&stripped, "%.%"));
}
if !bracketed && !format.opens_with().is_empty() {
conditions.push(not_like(&value, "_%-%"));
}
conditions
}
fn symbol_conditions(format: &NumberFormat, spelling: SidSpelling, value: &str) -> Vec<String> {
let prefix = spelling.lower.pattern_literal_prefix;
let like = |pattern: &str| format!("{value} LIKE {prefix}'{}'", pattern.replace('\'', "''"));
let not_like =
|pattern: &str| format!("{value} NOT LIKE {prefix}'{}'", pattern.replace('\'', "''"));
let mut conditions = Vec::new();
let bracketed = format.negatives() == Negatives::Parentheses;
let plain = |symbol: &str| !symbol.contains(['%', '_', '['].as_slice());
let opens = format.opens_with();
if !opens.is_empty() && plain(opens) {
let before = if bracketed { "(" } else { "-" };
conditions.push(not_like(&format!("%{opens}%{opens}%")));
conditions.push(format!(
"({} OR {} OR {})",
not_like(&format!("%{opens}%")),
like(&format!("{opens}%")),
like(&format!("{before}{opens}%")),
));
}
let closes = format.closes_with();
if !closes.is_empty() && plain(closes) {
let after = if bracketed { ")" } else { "" };
conditions.push(not_like(&format!("%{closes}%{closes}%")));
conditions.push(format!(
"({} OR {} OR {})",
not_like(&format!("%{closes}%")),
like(&format!("%{closes}")),
like(&format!("%{closes}{after}")),
));
}
conditions
}
fn thousands_after_a_digit(
format: &NumberFormat,
spelling: SidSpelling,
expr: &str,
) -> Vec<String> {
if !format.grouped() {
return Vec::new();
}
let prefix = spelling.lower.pattern_literal_prefix;
let value = normalize_trim(spelling, expr);
let sign = if format.negatives() == Negatives::Parentheses { "(" } else { "-" };
let opens = format.opens_with();
let mut openings = vec![String::new(), sign.to_owned()];
if !opens.is_empty() && !opens.contains(['%', '_', '['].as_slice()) {
openings.push(opens.to_owned());
openings.push(format!("{sign}{opens}"));
}
openings
.iter()
.map(|opening| {
format!(
"{value} NOT LIKE {prefix}'{}'",
format!("{opening}{THOUSANDS_MARK}%").replace('\'', "''")
)
})
.collect()
}
fn shape_conditions(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> Vec<String> {
let prefix = spelling.lower.pattern_literal_prefix;
let value = normalize_trim(spelling, expr);
let stripped = strip_marks(format, spelling, expr);
let mut conditions = symbol_conditions(format, spelling, &value);
conditions.push(format!("{} LIKE {prefix}'x0'", canonical_shape(format, spelling, &stripped)));
if format.negatives() != Negatives::Parentheses && !format.opens_with().is_empty() {
conditions.push(format!("{value} NOT LIKE {prefix}'_%-%'"));
}
conditions
}
fn canonical_shape(format: &NumberFormat, spelling: SidSpelling, stripped: &str) -> String {
let replace = spelling.lower.replace;
let prefix = spelling.lower.pattern_literal_prefix;
let swap = |text: String, from: &str, to: &str| {
format!("{replace}({text}, {prefix}'{from}', {prefix}'{to}')")
};
let zeros =
(1..10u8).fold(stripped.to_owned(), |text, digit| swap(text, &digit.to_string(), "0"));
let run = (0..8).fold(zeros, |text, _| swap(text, "00", "0"));
let pointed =
if format.decimals() > 0 { swap(run, &format!("0{DECIMAL_POINT}0"), "0") } else { run };
let lettered = join_text(spelling, &format!("{prefix}'x'"), &pointed);
if format.negatives() == Negatives::Parentheses {
lettered
} else {
swap(lettered, "x-", "x")
}
}
fn join_text(spelling: SidSpelling, left: &str, right: &str) -> String {
match spelling.concat {
Concat::Function(name) | Concat::FunctionBinary(name) => format!("{name}({left}, {right})"),
Concat::Plus => format!("{left} + {right}"),
Concat::Operator(operator) => format!("{left} {operator} {right}"),
}
}
fn bracket_sign(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> Option<String> {
if format.negatives() != Negatives::Parentheses {
return None;
}
Some(format!("CASE WHEN {} LIKE '(%' THEN -1 ELSE 1 END", normalize_trim(spelling, expr)))
}
fn signed(reading: String, sign: Option<String>) -> String {
match sign {
None => reading,
Some(sign) => format!("({reading}) * ({sign})"),
}
}
#[must_use]
pub fn read_with_safe_cast(
format: &NumberFormat,
spelling: SidSpelling,
expr: &str,
safe_cast: &dyn Fn(&str) -> String,
class: ClassTest<'_>,
) -> String {
let reading = safe_cast(&strip_marks(format, spelling, expr));
guarded_reading(format, spelling, expr, &reading, class)
}
fn guarded_reading(
format: &NumberFormat,
spelling: SidSpelling,
expr: &str,
reading: &str,
class: ClassTest<'_>,
) -> String {
let mut conditions = match class {
ClassTest::Flat(holds) => flat_shape_conditions(format, spelling, expr, holds),
ClassTest::Folded => shape_conditions(format, spelling, expr),
};
conditions.extend(separators_in_order(format, spelling, expr));
conditions.extend(thousands_after_a_digit(format, spelling, expr));
let guarded = format!("CASE WHEN {} THEN {reading} END", conditions.join(" AND "));
signed(guarded, bracket_sign(format, spelling, expr))
}
#[must_use]
pub fn read_with_pattern_guard(
format: &NumberFormat,
spelling: SidSpelling,
expr: &str,
cast: &dyn Fn(&str) -> String,
matches: &dyn Fn(&str, &str) -> String,
) -> String {
let trimmed = normalize_trim(spelling, expr);
let pattern = format!("'{}'", format.shape_pattern().replace('\'', "''"));
let reading = cast(&strip_marks(format, spelling, expr));
let guarded = format!("CASE WHEN {} THEN {reading} END", matches(&trimmed, &pattern));
signed(guarded, bracket_sign(format, spelling, expr))
}
#[must_use]
pub fn read_with_digit_check(
format: &NumberFormat,
spelling: SidSpelling,
expr: &str,
cast: &dyn Fn(&str) -> String,
class: ClassTest<'_>,
) -> String {
let reading = cast(&strip_marks(format, spelling, expr));
guarded_reading(format, spelling, expr, &reading, class)
}
#[cfg(test)]
mod tests {
use super::*;
fn like_class(text: &str, class: &str) -> String {
format!("{text} LIKE '%{class}%'")
}
const LIKE_CLASS: fn(&str, &str) -> String = like_class;
#[test]
fn a_price_picture_says_a_symbol_thousands_and_two_places() {
let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
assert_eq!(format.opens_with(), "$");
assert_eq!(format.closes_with(), "");
assert!(format.grouped());
assert_eq!(format.decimals(), 2);
assert_eq!(format.negatives(), Negatives::Minus);
assert_eq!(format.marks(), vec!["$".to_owned(), ",".to_owned()]);
}
#[test]
fn a_symbol_may_close_the_value_instead_of_opening_it() {
let format = NumberFormat::parse("#,##0.00 kr").expect("a lawful picture");
assert_eq!(format.opens_with(), "");
assert_eq!(format.closes_with(), " kr");
assert_eq!(format.marks(), vec![" kr".to_owned(), ",".to_owned()]);
}
#[test]
fn a_plain_picture_names_no_marks_at_all() {
let format = NumberFormat::parse("0.000").expect("a lawful picture");
assert!(!format.grouped());
assert_eq!(format.decimals(), 3);
assert!(format.marks().is_empty());
assert_eq!(format.shape_pattern(), "^-?[0-9]+([.][0-9]+)?$");
}
#[test]
fn a_ledger_negative_is_the_same_picture_in_brackets() {
let format = NumberFormat::parse("$#,##0.00;($#,##0.00)").expect("a lawful picture");
assert_eq!(format.negatives(), Negatives::Parentheses);
assert_eq!(
format.marks(),
vec!["$".to_owned(), ",".to_owned(), "(".to_owned(), ")".to_owned()]
);
assert_eq!(format.spelled(), "$#,##0.00;($#,##0.00)");
}
#[test]
fn the_shape_holds_the_symbol_and_the_groups_where_the_picture_put_them() {
let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
assert_eq!(format.shape_pattern(), "^-?([$])?[0-9][0-9,]*([.][0-9]+)?$");
}
#[test]
fn the_reading_holds_the_thousands_mark_in_front_of_the_decimal_point() {
let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
let read = read_with_safe_cast(
&format,
crate::ANSI_SID_SPELLING,
"v",
&|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
ClassTest::Folded,
);
assert!(read.contains("TRIM(v) NOT LIKE '%.%,%'"), "{read}");
}
#[test]
fn a_picture_with_no_thousands_mark_needs_no_such_condition() {
let format = NumberFormat::parse("0.00").expect("a lawful picture");
let read = read_with_safe_cast(
&format,
crate::ANSI_SID_SPELLING,
"v",
&|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
ClassTest::Folded,
);
assert!(!read.contains("'%.%,%'"), "no thousands mark to put in its place: {read}");
assert!(read.ends_with("THEN TRY_CAST(TRIM(v) AS DECIMAL(12, 2)) END"), "{read}");
}
#[test]
fn a_safe_cast_reading_holds_the_value_to_the_pictures_shape() {
let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
let read = read_with_safe_cast(
&format,
crate::ANSI_SID_SPELLING,
"v",
&|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
ClassTest::Folded,
);
for guard in [
"TRIM(v) NOT LIKE '%$%$%'",
"TRIM(v) LIKE '-$%'",
"'00', '0'), '0.0', '0'))",
"'x-', 'x') LIKE 'x0'",
"TRIM(v) NOT LIKE '_%-%'",
] {
assert!(read.contains(guard), "no {guard}: {read}");
}
let closing = NumberFormat::parse("#,##0.00 kr").expect("a lawful picture");
let read = read_with_safe_cast(
&closing,
crate::ANSI_SID_SPELLING,
"v",
&|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
ClassTest::Folded,
);
assert!(read.contains("TRIM(v) LIKE '% kr'"), "{read}");
}
#[test]
fn a_whole_numbers_digit_check_leaves_no_room_for_a_decimal_point() {
let whole = NumberFormat::parse("#0").expect("a lawful picture");
let read = read_with_digit_check(
&whole,
crate::ANSI_SID_SPELLING,
"v",
&|text| format!("CAST({text} AS INTEGER)"),
ClassTest::Folded,
);
assert!(read.contains("'x-', 'x') LIKE 'x0'") && !read.contains("'0.0', '0')"), "{read}");
let fractional = NumberFormat::parse("#0.0").expect("a lawful picture");
let read = read_with_digit_check(
&fractional,
crate::ANSI_SID_SPELLING,
"v",
&|text| format!("CAST({text} AS DECIMAL(18, 6))"),
ClassTest::Folded,
);
assert!(read.contains("'0.0', '0')), 'x-', 'x') LIKE 'x0'"), "{read}");
}
#[test]
fn a_reading_names_the_value_a_fixed_few_times() {
let value = "COLUMN_VALUE";
for picture in ["#0", "#0.0", "0.00"] {
let format = NumberFormat::parse(picture).expect("a lawful picture");
let read = read_with_safe_cast(
&format,
crate::ANSI_SID_SPELLING,
value,
&|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
ClassTest::Folded,
);
assert_eq!(read.matches(value).count(), 2, "{picture}: {read}");
}
}
fn depth(sql: &str) -> usize {
let (mut depth, mut deepest, mut quoted) = (0usize, 0usize, false);
for c in sql.chars() {
match c {
'\'' => quoted = !quoted,
'(' if !quoted => {
depth += 1;
deepest = deepest.max(depth);
}
')' if !quoted => depth = depth.saturating_sub(1),
_ => {}
}
}
deepest
}
#[test]
fn the_shared_reading_is_flat() {
let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
let read = read_with_safe_cast(
&format,
crate::ANSI_SID_SPELLING,
"v",
&|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
ClassTest::Flat(&LIKE_CLASS),
);
assert!(depth(&read) <= 6, "{} deep: {read}", depth(&read));
}
#[test]
fn a_value_opening_with_the_thousands_mark_is_set_aside() {
let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
for class in [ClassTest::Flat(&LIKE_CLASS), ClassTest::Folded] {
let read = read_with_safe_cast(
&format,
crate::ANSI_SID_SPELLING,
"v",
&|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
class,
);
for opening in ["',%'", "'-,%'", "'$,%'", "'-$,%'"] {
assert!(read.contains(&format!("TRIM(v) NOT LIKE {opening}")), "{opening}: {read}");
}
}
}
#[test]
fn a_bracketed_negative_is_read_as_a_sign_rather_than_a_mark() {
let format = NumberFormat::parse("$#,##0.00;($#,##0.00)").expect("a lawful picture");
let read = read_with_safe_cast(
&format,
crate::ANSI_SID_SPELLING,
"v",
&|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
ClassTest::Folded,
);
assert!(read.contains("CASE WHEN TRIM(v) LIKE '(%' THEN -1 ELSE 1 END"), "{read}");
assert!(read.contains("REPLACE(REPLACE(REPLACE(REPLACE(TRIM(v)"), "{read}");
}
#[test]
fn a_picture_with_no_digits_is_refused() {
assert_eq!(NumberFormat::parse("$"), Err(NumberFormatError::NoDigits));
assert_eq!(NumberFormat::parse(""), Err(NumberFormatError::Empty));
}
#[test]
fn a_source_writing_the_separators_the_other_way_round_is_refused_by_name() {
let refused = NumberFormat::parse("#.##0,00").expect_err("not covered");
assert_eq!(refused, NumberFormatError::MarkAfterPoint);
assert!(refused.to_string().contains("2.899,99"), "{refused}");
}
#[test]
fn a_thousands_mark_separates_groups_of_three() {
assert_eq!(NumberFormat::parse("#,##0,0"), Err(NumberFormatError::GroupIsNotThree(1)));
}
#[test]
fn two_decimal_points_are_refused() {
assert_eq!(NumberFormat::parse("0.0.0"), Err(NumberFormatError::TwoPoints));
}
#[test]
fn digits_on_both_sides_of_the_symbol_are_refused() {
assert_eq!(NumberFormat::parse("0 kr 0"), Err(NumberFormatError::DigitsAfterSymbol));
}
#[test]
fn a_second_picture_that_is_not_the_first_in_brackets_is_refused() {
assert_eq!(
NumberFormat::parse("$#,##0.00;-$#,##0.00"),
Err(NumberFormatError::NegativeIsNotBracketed)
);
assert_eq!(NumberFormat::parse("0;(0);0"), Err(NumberFormatError::TooManyPictures));
}
}