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
419fn shape_conditions(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> Vec<String> {
450 let prefix = spelling.lower.pattern_literal_prefix;
451 let value = normalize_trim(spelling, expr);
452 let stripped = strip_marks(format, spelling, expr);
453 let like = |subject: &str, pattern: &str| {
454 format!("{subject} LIKE {prefix}'{}'", pattern.replace('\'', "''"))
455 };
456 let not_like = |subject: &str, pattern: &str| {
457 format!("{subject} NOT LIKE {prefix}'{}'", pattern.replace('\'', "''"))
458 };
459 let mut conditions = Vec::new();
460 let bracketed = format.negatives() == Negatives::Parentheses;
461 let plain = |symbol: &str| !symbol.contains(['%', '_', '['].as_slice());
462 let opens = format.opens_with();
463 if !opens.is_empty() && plain(opens) {
464 let before = if bracketed { "(" } else { "-" };
465 conditions.push(not_like(&value, &format!("%{opens}%{opens}%")));
466 conditions.push(format!(
467 "({} OR {} OR {})",
468 not_like(&value, &format!("%{opens}%")),
469 like(&value, &format!("{opens}%")),
470 like(&value, &format!("{before}{opens}%")),
471 ));
472 }
473 let closes = format.closes_with();
474 if !closes.is_empty() && plain(closes) {
475 let after = if bracketed { ")" } else { "" };
476 conditions.push(not_like(&value, &format!("%{closes}%{closes}%")));
477 conditions.push(format!(
478 "({} OR {} OR {})",
479 not_like(&value, &format!("%{closes}%")),
480 like(&value, &format!("%{closes}")),
481 like(&value, &format!("%{closes}{after}")),
482 ));
483 }
484 conditions.push(like(&canonical_shape(format, spelling, &stripped), "x0"));
485 if !bracketed && !opens.is_empty() {
486 conditions.push(not_like(&value, "_%-%"));
487 }
488 conditions
489}
490
491fn canonical_shape(format: &NumberFormat, spelling: SidSpelling, stripped: &str) -> String {
505 let replace = spelling.lower.replace;
506 let prefix = spelling.lower.pattern_literal_prefix;
507 let swap = |text: String, from: &str, to: &str| {
508 format!("{replace}({text}, {prefix}'{from}', {prefix}'{to}')")
509 };
510 let zeros =
511 (1..10u8).fold(stripped.to_owned(), |text, digit| swap(text, &digit.to_string(), "0"));
512 let run = (0..8).fold(zeros, |text, _| swap(text, "00", "0"));
513 let pointed =
514 if format.decimals() > 0 { swap(run, &format!("0{DECIMAL_POINT}0"), "0") } else { run };
515 let lettered = join_text(spelling, &format!("{prefix}'x'"), &pointed);
516 if format.negatives() == Negatives::Parentheses {
517 lettered
519 } else {
520 swap(lettered, "x-", "x")
521 }
522}
523
524fn join_text(spelling: SidSpelling, left: &str, right: &str) -> String {
527 match spelling.concat {
528 Concat::Function(name) | Concat::FunctionBinary(name) => format!("{name}({left}, {right})"),
529 Concat::Plus => format!("{left} + {right}"),
530 Concat::Operator(operator) => format!("{left} {operator} {right}"),
531 }
532}
533
534fn bracket_sign(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> Option<String> {
540 if format.negatives() != Negatives::Parentheses {
541 return None;
542 }
543 Some(format!("CASE WHEN {} LIKE '(%' THEN -1 ELSE 1 END", normalize_trim(spelling, expr)))
544}
545
546fn signed(reading: String, sign: Option<String>) -> String {
548 match sign {
549 None => reading,
550 Some(sign) => format!("({reading}) * ({sign})"),
551 }
552}
553
554#[must_use]
568pub fn read_with_safe_cast(
569 format: &NumberFormat,
570 spelling: SidSpelling,
571 expr: &str,
572 safe_cast: &dyn Fn(&str) -> String,
573) -> String {
574 let reading = safe_cast(&strip_marks(format, spelling, expr));
575 let mut conditions = shape_conditions(format, spelling, expr);
576 conditions.extend(separators_in_order(format, spelling, expr));
577 let guarded = format!("CASE WHEN {} THEN {reading} END", conditions.join(" AND "));
578 signed(guarded, bracket_sign(format, spelling, expr))
579}
580
581#[must_use]
593pub fn read_with_pattern_guard(
594 format: &NumberFormat,
595 spelling: SidSpelling,
596 expr: &str,
597 cast: &dyn Fn(&str) -> String,
598 matches: &dyn Fn(&str, &str) -> String,
599) -> String {
600 let trimmed = normalize_trim(spelling, expr);
601 let pattern = format!("'{}'", format.shape_pattern().replace('\'', "''"));
602 let reading = cast(&strip_marks(format, spelling, expr));
603 let guarded = format!("CASE WHEN {} THEN {reading} END", matches(&trimmed, &pattern));
606 signed(guarded, bracket_sign(format, spelling, expr))
607}
608
609#[must_use]
625pub fn read_with_digit_check(
626 format: &NumberFormat,
627 spelling: SidSpelling,
628 expr: &str,
629 cast: &dyn Fn(&str) -> String,
630) -> String {
631 let reading = cast(&strip_marks(format, spelling, expr));
632 let mut conditions = shape_conditions(format, spelling, expr);
633 conditions.extend(separators_in_order(format, spelling, expr));
634 let guarded = format!("CASE WHEN {} THEN {reading} END", conditions.join(" AND "));
635 signed(guarded, bracket_sign(format, spelling, expr))
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 #[test]
643 fn a_price_picture_says_a_symbol_thousands_and_two_places() {
644 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
645 assert_eq!(format.opens_with(), "$");
646 assert_eq!(format.closes_with(), "");
647 assert!(format.grouped());
648 assert_eq!(format.decimals(), 2);
649 assert_eq!(format.negatives(), Negatives::Minus);
650 assert_eq!(format.marks(), vec!["$".to_owned(), ",".to_owned()]);
651 }
652
653 #[test]
654 fn a_symbol_may_close_the_value_instead_of_opening_it() {
655 let format = NumberFormat::parse("#,##0.00 kr").expect("a lawful picture");
656 assert_eq!(format.opens_with(), "");
657 assert_eq!(format.closes_with(), " kr");
658 assert_eq!(format.marks(), vec![" kr".to_owned(), ",".to_owned()]);
659 }
660
661 #[test]
662 fn a_plain_picture_names_no_marks_at_all() {
663 let format = NumberFormat::parse("0.000").expect("a lawful picture");
664 assert!(!format.grouped());
665 assert_eq!(format.decimals(), 3);
666 assert!(format.marks().is_empty());
667 assert_eq!(format.shape_pattern(), "^-?[0-9]+([.][0-9]+)?$");
668 }
669
670 #[test]
671 fn a_ledger_negative_is_the_same_picture_in_brackets() {
672 let format = NumberFormat::parse("$#,##0.00;($#,##0.00)").expect("a lawful picture");
673 assert_eq!(format.negatives(), Negatives::Parentheses);
674 assert_eq!(
675 format.marks(),
676 vec!["$".to_owned(), ",".to_owned(), "(".to_owned(), ")".to_owned()]
677 );
678 assert_eq!(format.spelled(), "$#,##0.00;($#,##0.00)");
679 }
680
681 #[test]
682 fn the_shape_holds_the_symbol_and_the_groups_where_the_picture_put_them() {
683 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
684 assert_eq!(format.shape_pattern(), "^-?([$])?[0-9][0-9,]*([.][0-9]+)?$");
685 }
686
687 #[test]
688 fn the_reading_holds_the_thousands_mark_in_front_of_the_decimal_point() {
689 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
690 let read = read_with_safe_cast(&format, crate::ANSI_SID_SPELLING, "v", &|text| {
691 format!("TRY_CAST({text} AS DECIMAL(12, 2))")
692 });
693 assert!(read.contains("TRIM(v) NOT LIKE '%.%,%'"), "{read}");
696 }
697
698 #[test]
699 fn a_picture_with_no_thousands_mark_needs_no_such_condition() {
700 let format = NumberFormat::parse("0.00").expect("a lawful picture");
701 let read = read_with_safe_cast(&format, crate::ANSI_SID_SPELLING, "v", &|text| {
702 format!("TRY_CAST({text} AS DECIMAL(12, 2))")
703 });
704 assert!(!read.contains("'%.%,%'"), "no thousands mark to put in its place: {read}");
705 assert!(read.ends_with("THEN TRY_CAST(TRIM(v) AS DECIMAL(12, 2)) END"), "{read}");
706 }
707
708 #[test]
715 fn a_safe_cast_reading_holds_the_value_to_the_pictures_shape() {
716 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
717 let read = read_with_safe_cast(&format, crate::ANSI_SID_SPELLING, "v", &|text| {
718 format!("TRY_CAST({text} AS DECIMAL(12, 2))")
719 });
720 for guard in [
721 "TRIM(v) NOT LIKE '%$%$%'",
722 "TRIM(v) LIKE '-$%'",
723 "'00', '0'), '0.0', '0'))",
724 "'x-', 'x') LIKE 'x0'",
725 "TRIM(v) NOT LIKE '_%-%'",
726 ] {
727 assert!(read.contains(guard), "no {guard}: {read}");
728 }
729 let closing = NumberFormat::parse("#,##0.00 kr").expect("a lawful picture");
730 let read = read_with_safe_cast(&closing, crate::ANSI_SID_SPELLING, "v", &|text| {
731 format!("TRY_CAST({text} AS DECIMAL(12, 2))")
732 });
733 assert!(read.contains("TRIM(v) LIKE '% kr'"), "{read}");
734 }
735
736 #[test]
737 fn a_whole_numbers_digit_check_leaves_no_room_for_a_decimal_point() {
738 let whole = NumberFormat::parse("#0").expect("a lawful picture");
739 let read = read_with_digit_check(&whole, crate::ANSI_SID_SPELLING, "v", &|text| {
740 format!("CAST({text} AS INTEGER)")
741 });
742 assert!(read.contains("'x-', 'x') LIKE 'x0'") && !read.contains("'0.0', '0')"), "{read}");
745 let fractional = NumberFormat::parse("#0.0").expect("a lawful picture");
746 let read = read_with_digit_check(&fractional, crate::ANSI_SID_SPELLING, "v", &|text| {
747 format!("CAST({text} AS DECIMAL(18, 6))")
748 });
749 assert!(read.contains("'0.0', '0')), 'x-', 'x') LIKE 'x0'"), "{read}");
750 }
751
752 #[test]
757 fn a_reading_names_the_value_a_fixed_few_times() {
758 let value = "COLUMN_VALUE";
759 for picture in ["#0", "#0.0", "0.00"] {
760 let format = NumberFormat::parse(picture).expect("a lawful picture");
761 let read = read_with_safe_cast(&format, crate::ANSI_SID_SPELLING, value, &|text| {
762 format!("TRY_CAST({text} AS DECIMAL(12, 2))")
763 });
764 assert_eq!(read.matches(value).count(), 2, "{picture}: {read}");
766 }
767 }
768
769 #[test]
770 fn a_bracketed_negative_is_read_as_a_sign_rather_than_a_mark() {
771 let format = NumberFormat::parse("$#,##0.00;($#,##0.00)").expect("a lawful picture");
772 let read = read_with_safe_cast(&format, crate::ANSI_SID_SPELLING, "v", &|text| {
773 format!("TRY_CAST({text} AS DECIMAL(12, 2))")
774 });
775 assert!(read.contains("CASE WHEN TRIM(v) LIKE '(%' THEN -1 ELSE 1 END"), "{read}");
776 assert!(read.contains("REPLACE(REPLACE(REPLACE(REPLACE(TRIM(v)"), "{read}");
777 }
778
779 #[test]
780 fn a_picture_with_no_digits_is_refused() {
781 assert_eq!(NumberFormat::parse("$"), Err(NumberFormatError::NoDigits));
782 assert_eq!(NumberFormat::parse(""), Err(NumberFormatError::Empty));
783 }
784
785 #[test]
786 fn a_source_writing_the_separators_the_other_way_round_is_refused_by_name() {
787 let refused = NumberFormat::parse("#.##0,00").expect_err("not covered");
788 assert_eq!(refused, NumberFormatError::MarkAfterPoint);
789 assert!(refused.to_string().contains("2.899,99"), "{refused}");
790 }
791
792 #[test]
793 fn a_thousands_mark_separates_groups_of_three() {
794 assert_eq!(NumberFormat::parse("#,##0,0"), Err(NumberFormatError::GroupIsNotThree(1)));
795 }
796
797 #[test]
798 fn two_decimal_points_are_refused() {
799 assert_eq!(NumberFormat::parse("0.0.0"), Err(NumberFormatError::TwoPoints));
800 }
801
802 #[test]
803 fn digits_on_both_sides_of_the_symbol_are_refused() {
804 assert_eq!(NumberFormat::parse("0 kr 0"), Err(NumberFormatError::DigitsAfterSymbol));
805 }
806
807 #[test]
808 fn a_second_picture_that_is_not_the_first_in_brackets_is_refused() {
809 assert_eq!(
810 NumberFormat::parse("$#,##0.00;-$#,##0.00"),
811 Err(NumberFormatError::NegativeIsNotBracketed)
812 );
813 assert_eq!(NumberFormat::parse("0;(0);0"), Err(NumberFormatError::TooManyPictures));
814 }
815}