1use std::fmt;
56
57use crate::DataType;
58use crate::sid::normalize_trim;
59use crate::sid_spelling::{Concat, SidSpelling};
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Negatives {
64 Minus,
67 Parentheses,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum NumberFormatError {
75 Empty,
77 NoDigits,
79 MarkAfterPoint,
81 TwoPoints,
83 GroupIsNotThree(usize),
85 DigitsAfterSymbol,
87 TooManyPictures,
89 NegativeIsNotBracketed,
91}
92
93impl fmt::Display for NumberFormatError {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 Self::Empty => f.write_str(
97 "the format is empty. Write how the source writes the value, with its digits \
98 replaced by digit places: `$#,##0.00` for `$2,899.99`, `0.000` for `1.158`",
99 ),
100 Self::NoDigits => f.write_str(
101 "the format holds no digit place, so there is no number in it. A digit place is \
102 `0` or `#`: `$#,##0.00` for `$2,899.99`",
103 ),
104 Self::MarkAfterPoint => f.write_str(
105 "the format puts a `,` after the `.`. In a picture `,` is the thousands mark and \
106 `.` is the decimal point, whichever characters the source itself uses, so the \
107 thousands come first. A source that writes `2.899,99` is not covered today",
108 ),
109 Self::TwoPoints => f.write_str(
110 "the format has two decimal points, and a number has one. If one of them is the \
111 thousands mark, write it as `,`: `$#,##0.00` is two thousand eight hundred and \
112 ninety-nine point nine nine",
113 ),
114 Self::GroupIsNotThree(places) => write!(
115 f,
116 "the format puts {places} digit {} after a `,`, and a thousands mark \
117 separates groups of three. Write `#,##0` for a number written `2,899`",
118 if *places == 1 { "place" } else { "places" }
119 ),
120 Self::DigitsAfterSymbol => f.write_str(
121 "the format has digit places on both sides of the symbol. The symbol opens the \
122 value or closes it (`$#,##0.00` or `#,##0.00 kr`) and a number written in two \
123 halves is not covered",
124 ),
125 Self::TooManyPictures => f.write_str(
126 "the format holds more than two pictures. One picture says how the source writes \
127 a value; a second, after a `;`, says how it writes a negative one",
128 ),
129 Self::NegativeIsNotBracketed => f.write_str(
130 "the second picture is not the first one in brackets. It says the source writes a \
131 negative the way a ledger does, so it is the first picture inside `(` and `)`: \
132 `$#,##0.00;($#,##0.00)`. A source that writes a leading minus needs no second \
133 picture at all",
134 ),
135 }
136 }
137}
138
139impl std::error::Error for NumberFormatError {}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct NumberFormat {
147 spelled: String,
148 opens_with: String,
149 closes_with: String,
150 grouped: bool,
151 decimals: usize,
152 negatives: Negatives,
153}
154
155const DIGIT_PLACES: [char; 2] = ['#', '0'];
157const THOUSANDS_MARK: char = ',';
158const DECIMAL_POINT: char = '.';
159
160impl NumberFormat {
161 pub fn parse(spelled: &str) -> Result<Self, NumberFormatError> {
170 if spelled.is_empty() {
171 return Err(NumberFormatError::Empty);
172 }
173 let mut pictures = spelled.split(';');
174 let positive = pictures.next().unwrap_or_default();
175 let negative = pictures.next();
176 if pictures.next().is_some() {
177 return Err(NumberFormatError::TooManyPictures);
178 }
179 let mut read = Self::read_one(positive)?;
180 if let Some(negative) = negative {
181 if negative != format!("({positive})") {
182 return Err(NumberFormatError::NegativeIsNotBracketed);
183 }
184 read.negatives = Negatives::Parentheses;
185 }
186 spelled.clone_into(&mut read.spelled);
187 Ok(read)
188 }
189
190 fn read_one(picture: &str) -> Result<Self, NumberFormatError> {
193 let is_body = |c: char| DIGIT_PLACES.contains(&c) || c == THOUSANDS_MARK;
194 let first =
195 picture.find(|c| DIGIT_PLACES.contains(&c)).ok_or(NumberFormatError::NoDigits)?;
196 let opens_with = picture[..first].to_owned();
197 let rest = &picture[first..];
198
199 let mut body = String::new();
202 let mut seen_point = false;
203 let mut closes_at = rest.len();
204 for (at, character) in rest.char_indices() {
205 if is_body(character) {
206 if seen_point && character == THOUSANDS_MARK {
207 return Err(NumberFormatError::MarkAfterPoint);
208 }
209 body.push(character);
210 continue;
211 }
212 if character == DECIMAL_POINT {
213 if seen_point {
214 return Err(NumberFormatError::TwoPoints);
215 }
216 seen_point = true;
217 body.push(character);
218 continue;
219 }
220 closes_at = at;
221 break;
222 }
223 let closes_with = rest[closes_at..].to_owned();
224 if closes_with.contains(DIGIT_PLACES) {
225 return Err(NumberFormatError::DigitsAfterSymbol);
226 }
227
228 let (whole, fraction) = match body.split_once(DECIMAL_POINT) {
229 Some((whole, fraction)) => (whole, fraction),
230 None => (body.as_str(), ""),
231 };
232 if fraction.contains(THOUSANDS_MARK) {
233 return Err(NumberFormatError::MarkAfterPoint);
234 }
235 let grouped = whole.contains(THOUSANDS_MARK);
236 if grouped {
237 for group in whole.split(THOUSANDS_MARK).skip(1) {
238 if group.chars().count() != 3 {
239 return Err(NumberFormatError::GroupIsNotThree(group.chars().count()));
240 }
241 }
242 }
243
244 Ok(Self {
245 spelled: picture.to_owned(),
246 opens_with,
247 closes_with,
248 grouped,
249 decimals: fraction.chars().count(),
250 negatives: Negatives::Minus,
251 })
252 }
253
254 #[must_use]
256 pub fn spelled(&self) -> &str {
257 &self.spelled
258 }
259
260 #[must_use]
262 pub fn opens_with(&self) -> &str {
263 &self.opens_with
264 }
265
266 #[must_use]
268 pub fn closes_with(&self) -> &str {
269 &self.closes_with
270 }
271
272 #[must_use]
274 pub fn grouped(&self) -> bool {
275 self.grouped
276 }
277
278 #[must_use]
280 pub fn decimals(&self) -> usize {
281 self.decimals
282 }
283
284 #[must_use]
286 pub fn negatives(&self) -> Negatives {
287 self.negatives
288 }
289
290 #[must_use]
298 pub fn marks(&self) -> Vec<String> {
299 let mut marks = Vec::new();
300 if !self.opens_with.is_empty() {
301 marks.push(self.opens_with.clone());
302 }
303 if !self.closes_with.is_empty() {
304 marks.push(self.closes_with.clone());
305 }
306 if self.grouped {
307 marks.push(THOUSANDS_MARK.to_string());
308 }
309 if self.negatives == Negatives::Parentheses {
310 marks.push("(".to_owned());
311 marks.push(")".to_owned());
312 }
313 marks
314 }
315
316 #[must_use]
323 pub fn shape_pattern(&self) -> String {
324 let digits = if self.grouped { "[0-9][0-9,]*" } else { "[0-9]+" };
325 let fraction = if self.decimals > 0 { "([.][0-9]+)?" } else { "" };
326 let (open, close) = match self.negatives {
327 Negatives::Minus => ("-?", ""),
328 Negatives::Parentheses => ("[(]?", "[)]?"),
329 };
330 format!(
331 "^{open}{}{digits}{fraction}{}{close}$",
332 optional_literal(&self.opens_with),
333 optional_literal(&self.closes_with),
334 )
335 }
336}
337
338impl fmt::Display for NumberFormat {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 f.write_str(&self.spelled)
341 }
342}
343
344fn optional_literal(text: &str) -> String {
350 if text.is_empty() {
351 return String::new();
352 }
353 let mut pattern = String::new();
354 for character in text.chars() {
355 if character.is_ascii_alphanumeric() {
356 pattern.push(character);
357 } else {
358 pattern.push('[');
359 pattern.push(character);
360 pattern.push(']');
361 }
362 }
363 format!("({pattern})?")
364}
365
366#[must_use]
372pub fn is_a_number(target: &DataType) -> bool {
373 matches!(target, DataType::Integer(_) | DataType::Decimal(_) | DataType::Float(_))
374}
375
376#[must_use]
388pub fn strip_marks(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> String {
389 let replace = spelling.lower.replace;
390 let prefix = spelling.lower.pattern_literal_prefix;
391 format.marks().iter().fold(normalize_trim(spelling, expr), |text, mark| {
392 format!("{replace}({text}, {prefix}'{}', {prefix}'')", mark.replace('\'', "''"))
393 })
394}
395
396fn separators_in_order(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> Option<String> {
409 if !format.grouped() {
410 return None;
411 }
412 let prefix = spelling.lower.pattern_literal_prefix;
413 Some(format!(
414 "{} NOT LIKE {prefix}'%{DECIMAL_POINT}%{THOUSANDS_MARK}%'",
415 normalize_trim(spelling, expr)
416 ))
417}
418
419#[derive(Clone, Copy)]
429pub enum ClassTest<'a> {
430 Flat(&'a dyn Fn(&str, &str) -> String),
436 Folded,
441}
442
443fn flat_shape_conditions(
448 format: &NumberFormat,
449 spelling: SidSpelling,
450 expr: &str,
451 holds: &dyn Fn(&str, &str) -> String,
452) -> Vec<String> {
453 let prefix = spelling.lower.pattern_literal_prefix;
454 let value = normalize_trim(spelling, expr);
455 let stripped = strip_marks(format, spelling, expr);
456 let not_like = |subject: &str, pattern: &str| {
457 format!("{subject} NOT LIKE {prefix}'{}'", pattern.replace('\'', "''"))
458 };
459 let mut conditions = symbol_conditions(format, spelling, &value);
460 conditions.push(holds(&stripped, "[0-9]"));
461 conditions.push(format!("NOT {}", holds(&stripped, "[^0-9.-]")));
462 let bracketed = format.negatives() == Negatives::Parentheses;
463 conditions.push(not_like(&stripped, if bracketed { "%-%" } else { "_%-%" }));
464 if format.decimals() > 0 {
465 for shape in ["%.%.%", ".%", "-.%", "%."] {
466 conditions.push(not_like(&stripped, shape));
467 }
468 } else {
469 conditions.push(not_like(&stripped, "%.%"));
470 }
471 if !bracketed && !format.opens_with().is_empty() {
472 conditions.push(not_like(&value, "_%-%"));
473 }
474 conditions
475}
476
477fn symbol_conditions(format: &NumberFormat, spelling: SidSpelling, value: &str) -> Vec<String> {
482 let prefix = spelling.lower.pattern_literal_prefix;
483 let like = |pattern: &str| format!("{value} LIKE {prefix}'{}'", pattern.replace('\'', "''"));
484 let not_like =
485 |pattern: &str| format!("{value} NOT LIKE {prefix}'{}'", pattern.replace('\'', "''"));
486 let mut conditions = Vec::new();
487 let bracketed = format.negatives() == Negatives::Parentheses;
488 let plain = |symbol: &str| !symbol.contains(['%', '_', '['].as_slice());
489 let opens = format.opens_with();
490 if !opens.is_empty() && plain(opens) {
491 let before = if bracketed { "(" } else { "-" };
492 conditions.push(not_like(&format!("%{opens}%{opens}%")));
493 conditions.push(format!(
494 "({} OR {} OR {})",
495 not_like(&format!("%{opens}%")),
496 like(&format!("{opens}%")),
497 like(&format!("{before}{opens}%")),
498 ));
499 }
500 let closes = format.closes_with();
501 if !closes.is_empty() && plain(closes) {
502 let after = if bracketed { ")" } else { "" };
503 conditions.push(not_like(&format!("%{closes}%{closes}%")));
504 conditions.push(format!(
505 "({} OR {} OR {})",
506 not_like(&format!("%{closes}%")),
507 like(&format!("%{closes}")),
508 like(&format!("%{closes}{after}")),
509 ));
510 }
511 conditions
512}
513
514fn thousands_after_a_digit(
520 format: &NumberFormat,
521 spelling: SidSpelling,
522 expr: &str,
523) -> Vec<String> {
524 if !format.grouped() {
525 return Vec::new();
526 }
527 let prefix = spelling.lower.pattern_literal_prefix;
528 let value = normalize_trim(spelling, expr);
529 let sign = if format.negatives() == Negatives::Parentheses { "(" } else { "-" };
530 let opens = format.opens_with();
531 let mut openings = vec![String::new(), sign.to_owned()];
532 if !opens.is_empty() && !opens.contains(['%', '_', '['].as_slice()) {
533 openings.push(opens.to_owned());
534 openings.push(format!("{sign}{opens}"));
535 }
536 openings
537 .iter()
538 .map(|opening| {
539 format!(
540 "{value} NOT LIKE {prefix}'{}'",
541 format!("{opening}{THOUSANDS_MARK}%").replace('\'', "''")
542 )
543 })
544 .collect()
545}
546
547fn shape_conditions(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> Vec<String> {
578 let prefix = spelling.lower.pattern_literal_prefix;
579 let value = normalize_trim(spelling, expr);
580 let stripped = strip_marks(format, spelling, expr);
581 let mut conditions = symbol_conditions(format, spelling, &value);
582 conditions.push(format!("{} LIKE {prefix}'x0'", canonical_shape(format, spelling, &stripped)));
583 if format.negatives() != Negatives::Parentheses && !format.opens_with().is_empty() {
584 conditions.push(format!("{value} NOT LIKE {prefix}'_%-%'"));
585 }
586 conditions
587}
588
589fn canonical_shape(format: &NumberFormat, spelling: SidSpelling, stripped: &str) -> String {
603 let replace = spelling.lower.replace;
604 let prefix = spelling.lower.pattern_literal_prefix;
605 let swap = |text: String, from: &str, to: &str| {
606 format!("{replace}({text}, {prefix}'{from}', {prefix}'{to}')")
607 };
608 let zeros =
609 (1..10u8).fold(stripped.to_owned(), |text, digit| swap(text, &digit.to_string(), "0"));
610 let run = (0..8).fold(zeros, |text, _| swap(text, "00", "0"));
611 let pointed =
612 if format.decimals() > 0 { swap(run, &format!("0{DECIMAL_POINT}0"), "0") } else { run };
613 let lettered = join_text(spelling, &format!("{prefix}'x'"), &pointed);
614 if format.negatives() == Negatives::Parentheses {
615 lettered
617 } else {
618 swap(lettered, "x-", "x")
619 }
620}
621
622fn join_text(spelling: SidSpelling, left: &str, right: &str) -> String {
625 match spelling.concat {
626 Concat::Function(name) | Concat::FunctionBinary(name) => format!("{name}({left}, {right})"),
627 Concat::Plus => format!("{left} + {right}"),
628 Concat::Operator(operator) => format!("{left} {operator} {right}"),
629 }
630}
631
632fn bracket_sign(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> Option<String> {
638 if format.negatives() != Negatives::Parentheses {
639 return None;
640 }
641 Some(format!("CASE WHEN {} LIKE '(%' THEN -1 ELSE 1 END", normalize_trim(spelling, expr)))
642}
643
644fn signed(reading: String, sign: Option<String>) -> String {
646 match sign {
647 None => reading,
648 Some(sign) => format!("({reading}) * ({sign})"),
649 }
650}
651
652#[must_use]
669pub fn read_with_safe_cast(
670 format: &NumberFormat,
671 spelling: SidSpelling,
672 expr: &str,
673 safe_cast: &dyn Fn(&str) -> String,
674 class: ClassTest<'_>,
675) -> String {
676 let reading = safe_cast(&strip_marks(format, spelling, expr));
677 guarded_reading(format, spelling, expr, &reading, class)
678}
679
680fn guarded_reading(
683 format: &NumberFormat,
684 spelling: SidSpelling,
685 expr: &str,
686 reading: &str,
687 class: ClassTest<'_>,
688) -> String {
689 let mut conditions = match class {
690 ClassTest::Flat(holds) => flat_shape_conditions(format, spelling, expr, holds),
691 ClassTest::Folded => shape_conditions(format, spelling, expr),
692 };
693 conditions.extend(separators_in_order(format, spelling, expr));
694 conditions.extend(thousands_after_a_digit(format, spelling, expr));
695 let guarded = format!("CASE WHEN {} THEN {reading} END", conditions.join(" AND "));
696 signed(guarded, bracket_sign(format, spelling, expr))
697}
698
699#[must_use]
711pub fn read_with_pattern_guard(
712 format: &NumberFormat,
713 spelling: SidSpelling,
714 expr: &str,
715 cast: &dyn Fn(&str) -> String,
716 matches: &dyn Fn(&str, &str) -> String,
717) -> String {
718 let trimmed = normalize_trim(spelling, expr);
719 let pattern = format!("'{}'", format.shape_pattern().replace('\'', "''"));
720 let reading = cast(&strip_marks(format, spelling, expr));
721 let guarded = format!("CASE WHEN {} THEN {reading} END", matches(&trimmed, &pattern));
724 signed(guarded, bracket_sign(format, spelling, expr))
725}
726
727#[must_use]
744pub fn read_with_digit_check(
745 format: &NumberFormat,
746 spelling: SidSpelling,
747 expr: &str,
748 cast: &dyn Fn(&str) -> String,
749 class: ClassTest<'_>,
750) -> String {
751 let reading = cast(&strip_marks(format, spelling, expr));
752 guarded_reading(format, spelling, expr, &reading, class)
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 fn like_class(text: &str, class: &str) -> String {
761 format!("{text} LIKE '%{class}%'")
762 }
763 const LIKE_CLASS: fn(&str, &str) -> String = like_class;
764
765 #[test]
766 fn a_price_picture_says_a_symbol_thousands_and_two_places() {
767 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
768 assert_eq!(format.opens_with(), "$");
769 assert_eq!(format.closes_with(), "");
770 assert!(format.grouped());
771 assert_eq!(format.decimals(), 2);
772 assert_eq!(format.negatives(), Negatives::Minus);
773 assert_eq!(format.marks(), vec!["$".to_owned(), ",".to_owned()]);
774 }
775
776 #[test]
777 fn a_symbol_may_close_the_value_instead_of_opening_it() {
778 let format = NumberFormat::parse("#,##0.00 kr").expect("a lawful picture");
779 assert_eq!(format.opens_with(), "");
780 assert_eq!(format.closes_with(), " kr");
781 assert_eq!(format.marks(), vec![" kr".to_owned(), ",".to_owned()]);
782 }
783
784 #[test]
785 fn a_plain_picture_names_no_marks_at_all() {
786 let format = NumberFormat::parse("0.000").expect("a lawful picture");
787 assert!(!format.grouped());
788 assert_eq!(format.decimals(), 3);
789 assert!(format.marks().is_empty());
790 assert_eq!(format.shape_pattern(), "^-?[0-9]+([.][0-9]+)?$");
791 }
792
793 #[test]
794 fn a_ledger_negative_is_the_same_picture_in_brackets() {
795 let format = NumberFormat::parse("$#,##0.00;($#,##0.00)").expect("a lawful picture");
796 assert_eq!(format.negatives(), Negatives::Parentheses);
797 assert_eq!(
798 format.marks(),
799 vec!["$".to_owned(), ",".to_owned(), "(".to_owned(), ")".to_owned()]
800 );
801 assert_eq!(format.spelled(), "$#,##0.00;($#,##0.00)");
802 }
803
804 #[test]
805 fn the_shape_holds_the_symbol_and_the_groups_where_the_picture_put_them() {
806 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
807 assert_eq!(format.shape_pattern(), "^-?([$])?[0-9][0-9,]*([.][0-9]+)?$");
808 }
809
810 #[test]
811 fn the_reading_holds_the_thousands_mark_in_front_of_the_decimal_point() {
812 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
813 let read = read_with_safe_cast(
814 &format,
815 crate::ANSI_SID_SPELLING,
816 "v",
817 &|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
818 ClassTest::Folded,
819 );
820 assert!(read.contains("TRIM(v) NOT LIKE '%.%,%'"), "{read}");
823 }
824
825 #[test]
826 fn a_picture_with_no_thousands_mark_needs_no_such_condition() {
827 let format = NumberFormat::parse("0.00").expect("a lawful picture");
828 let read = read_with_safe_cast(
829 &format,
830 crate::ANSI_SID_SPELLING,
831 "v",
832 &|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
833 ClassTest::Folded,
834 );
835 assert!(!read.contains("'%.%,%'"), "no thousands mark to put in its place: {read}");
836 assert!(read.ends_with("THEN TRY_CAST(TRIM(v) AS DECIMAL(12, 2)) END"), "{read}");
837 }
838
839 #[test]
846 fn a_safe_cast_reading_holds_the_value_to_the_pictures_shape() {
847 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
848 let read = read_with_safe_cast(
849 &format,
850 crate::ANSI_SID_SPELLING,
851 "v",
852 &|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
853 ClassTest::Folded,
854 );
855 for guard in [
856 "TRIM(v) NOT LIKE '%$%$%'",
857 "TRIM(v) LIKE '-$%'",
858 "'00', '0'), '0.0', '0'))",
859 "'x-', 'x') LIKE 'x0'",
860 "TRIM(v) NOT LIKE '_%-%'",
861 ] {
862 assert!(read.contains(guard), "no {guard}: {read}");
863 }
864 let closing = NumberFormat::parse("#,##0.00 kr").expect("a lawful picture");
865 let read = read_with_safe_cast(
866 &closing,
867 crate::ANSI_SID_SPELLING,
868 "v",
869 &|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
870 ClassTest::Folded,
871 );
872 assert!(read.contains("TRIM(v) LIKE '% kr'"), "{read}");
873 }
874
875 #[test]
876 fn a_whole_numbers_digit_check_leaves_no_room_for_a_decimal_point() {
877 let whole = NumberFormat::parse("#0").expect("a lawful picture");
878 let read = read_with_digit_check(
879 &whole,
880 crate::ANSI_SID_SPELLING,
881 "v",
882 &|text| format!("CAST({text} AS INTEGER)"),
883 ClassTest::Folded,
884 );
885 assert!(read.contains("'x-', 'x') LIKE 'x0'") && !read.contains("'0.0', '0')"), "{read}");
888 let fractional = NumberFormat::parse("#0.0").expect("a lawful picture");
889 let read = read_with_digit_check(
890 &fractional,
891 crate::ANSI_SID_SPELLING,
892 "v",
893 &|text| format!("CAST({text} AS DECIMAL(18, 6))"),
894 ClassTest::Folded,
895 );
896 assert!(read.contains("'0.0', '0')), 'x-', 'x') LIKE 'x0'"), "{read}");
897 }
898
899 #[test]
904 fn a_reading_names_the_value_a_fixed_few_times() {
905 let value = "COLUMN_VALUE";
906 for picture in ["#0", "#0.0", "0.00"] {
907 let format = NumberFormat::parse(picture).expect("a lawful picture");
908 let read = read_with_safe_cast(
909 &format,
910 crate::ANSI_SID_SPELLING,
911 value,
912 &|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
913 ClassTest::Folded,
914 );
915 assert_eq!(read.matches(value).count(), 2, "{picture}: {read}");
917 }
918 }
919
920 fn depth(sql: &str) -> usize {
923 let (mut depth, mut deepest, mut quoted) = (0usize, 0usize, false);
924 for c in sql.chars() {
925 match c {
926 '\'' => quoted = !quoted,
927 '(' if !quoted => {
928 depth += 1;
929 deepest = deepest.max(depth);
930 }
931 ')' if !quoted => depth = depth.saturating_sub(1),
932 _ => {}
933 }
934 }
935 deepest
936 }
937
938 #[test]
943 fn the_shared_reading_is_flat() {
944 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
945 let read = read_with_safe_cast(
946 &format,
947 crate::ANSI_SID_SPELLING,
948 "v",
949 &|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
950 ClassTest::Flat(&LIKE_CLASS),
951 );
952 assert!(depth(&read) <= 6, "{} deep: {read}", depth(&read));
953 }
954
955 #[test]
959 fn a_value_opening_with_the_thousands_mark_is_set_aside() {
960 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
961 for class in [ClassTest::Flat(&LIKE_CLASS), ClassTest::Folded] {
962 let read = read_with_safe_cast(
963 &format,
964 crate::ANSI_SID_SPELLING,
965 "v",
966 &|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
967 class,
968 );
969 for opening in ["',%'", "'-,%'", "'$,%'", "'-$,%'"] {
970 assert!(read.contains(&format!("TRIM(v) NOT LIKE {opening}")), "{opening}: {read}");
971 }
972 }
973 }
974
975 #[test]
976 fn a_bracketed_negative_is_read_as_a_sign_rather_than_a_mark() {
977 let format = NumberFormat::parse("$#,##0.00;($#,##0.00)").expect("a lawful picture");
978 let read = read_with_safe_cast(
979 &format,
980 crate::ANSI_SID_SPELLING,
981 "v",
982 &|text| format!("TRY_CAST({text} AS DECIMAL(12, 2))"),
983 ClassTest::Folded,
984 );
985 assert!(read.contains("CASE WHEN TRIM(v) LIKE '(%' THEN -1 ELSE 1 END"), "{read}");
986 assert!(read.contains("REPLACE(REPLACE(REPLACE(REPLACE(TRIM(v)"), "{read}");
987 }
988
989 #[test]
990 fn a_picture_with_no_digits_is_refused() {
991 assert_eq!(NumberFormat::parse("$"), Err(NumberFormatError::NoDigits));
992 assert_eq!(NumberFormat::parse(""), Err(NumberFormatError::Empty));
993 }
994
995 #[test]
996 fn a_source_writing_the_separators_the_other_way_round_is_refused_by_name() {
997 let refused = NumberFormat::parse("#.##0,00").expect_err("not covered");
998 assert_eq!(refused, NumberFormatError::MarkAfterPoint);
999 assert!(refused.to_string().contains("2.899,99"), "{refused}");
1000 }
1001
1002 #[test]
1003 fn a_thousands_mark_separates_groups_of_three() {
1004 assert_eq!(NumberFormat::parse("#,##0,0"), Err(NumberFormatError::GroupIsNotThree(1)));
1005 }
1006
1007 #[test]
1008 fn two_decimal_points_are_refused() {
1009 assert_eq!(NumberFormat::parse("0.0.0"), Err(NumberFormatError::TwoPoints));
1010 }
1011
1012 #[test]
1013 fn digits_on_both_sides_of_the_symbol_are_refused() {
1014 assert_eq!(NumberFormat::parse("0 kr 0"), Err(NumberFormatError::DigitsAfterSymbol));
1015 }
1016
1017 #[test]
1018 fn a_second_picture_that_is_not_the_first_in_brackets_is_refused() {
1019 assert_eq!(
1020 NumberFormat::parse("$#,##0.00;-$#,##0.00"),
1021 Err(NumberFormatError::NegativeIsNotBracketed)
1022 );
1023 assert_eq!(NumberFormat::parse("0;(0);0"), Err(NumberFormatError::TooManyPictures));
1024 }
1025}