1use std::fmt;
56
57use crate::DataType;
58use crate::sid::normalize_trim;
59use crate::sid_spelling::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 place(s) after a `,`, and a thousands mark \
117 separates groups of three. Write `#,##0` for a number written `2,899`"
118 ),
119 Self::DigitsAfterSymbol => f.write_str(
120 "the format has digit places on both sides of the symbol. The symbol opens the \
121 value or closes it — `$#,##0.00` or `#,##0.00 kr` — and a number written in two \
122 halves is not covered",
123 ),
124 Self::TooManyPictures => f.write_str(
125 "the format holds more than two pictures. One picture says how the source writes \
126 a value; a second, after a `;`, says how it writes a negative one",
127 ),
128 Self::NegativeIsNotBracketed => f.write_str(
129 "the second picture is not the first one in brackets. It says the source writes a \
130 negative the way a ledger does, so it is the first picture inside `(` and `)` — \
131 `$#,##0.00;($#,##0.00)`. A source that writes a leading minus needs no second \
132 picture at all",
133 ),
134 }
135 }
136}
137
138impl std::error::Error for NumberFormatError {}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct NumberFormat {
146 spelled: String,
147 opens_with: String,
148 closes_with: String,
149 grouped: bool,
150 decimals: usize,
151 negatives: Negatives,
152}
153
154const DIGIT_PLACES: [char; 2] = ['#', '0'];
156const THOUSANDS_MARK: char = ',';
157const DECIMAL_POINT: char = '.';
158
159impl NumberFormat {
160 pub fn parse(spelled: &str) -> Result<Self, NumberFormatError> {
169 if spelled.is_empty() {
170 return Err(NumberFormatError::Empty);
171 }
172 let mut pictures = spelled.split(';');
173 let positive = pictures.next().unwrap_or_default();
174 let negative = pictures.next();
175 if pictures.next().is_some() {
176 return Err(NumberFormatError::TooManyPictures);
177 }
178 let mut read = Self::read_one(positive)?;
179 if let Some(negative) = negative {
180 if negative != format!("({positive})") {
181 return Err(NumberFormatError::NegativeIsNotBracketed);
182 }
183 read.negatives = Negatives::Parentheses;
184 }
185 spelled.clone_into(&mut read.spelled);
186 Ok(read)
187 }
188
189 fn read_one(picture: &str) -> Result<Self, NumberFormatError> {
192 let is_body = |c: char| DIGIT_PLACES.contains(&c) || c == THOUSANDS_MARK;
193 let first =
194 picture.find(|c| DIGIT_PLACES.contains(&c)).ok_or(NumberFormatError::NoDigits)?;
195 let opens_with = picture[..first].to_owned();
196 let rest = &picture[first..];
197
198 let mut body = String::new();
201 let mut seen_point = false;
202 let mut closes_at = rest.len();
203 for (at, character) in rest.char_indices() {
204 if is_body(character) {
205 if seen_point && character == THOUSANDS_MARK {
206 return Err(NumberFormatError::MarkAfterPoint);
207 }
208 body.push(character);
209 continue;
210 }
211 if character == DECIMAL_POINT {
212 if seen_point {
213 return Err(NumberFormatError::TwoPoints);
214 }
215 seen_point = true;
216 body.push(character);
217 continue;
218 }
219 closes_at = at;
220 break;
221 }
222 let closes_with = rest[closes_at..].to_owned();
223 if closes_with.contains(DIGIT_PLACES) {
224 return Err(NumberFormatError::DigitsAfterSymbol);
225 }
226
227 let (whole, fraction) = match body.split_once(DECIMAL_POINT) {
228 Some((whole, fraction)) => (whole, fraction),
229 None => (body.as_str(), ""),
230 };
231 if fraction.contains(THOUSANDS_MARK) {
232 return Err(NumberFormatError::MarkAfterPoint);
233 }
234 let grouped = whole.contains(THOUSANDS_MARK);
235 if grouped {
236 for group in whole.split(THOUSANDS_MARK).skip(1) {
237 if group.chars().count() != 3 {
238 return Err(NumberFormatError::GroupIsNotThree(group.chars().count()));
239 }
240 }
241 }
242
243 Ok(Self {
244 spelled: picture.to_owned(),
245 opens_with,
246 closes_with,
247 grouped,
248 decimals: fraction.chars().count(),
249 negatives: Negatives::Minus,
250 })
251 }
252
253 #[must_use]
255 pub fn spelled(&self) -> &str {
256 &self.spelled
257 }
258
259 #[must_use]
261 pub fn opens_with(&self) -> &str {
262 &self.opens_with
263 }
264
265 #[must_use]
267 pub fn closes_with(&self) -> &str {
268 &self.closes_with
269 }
270
271 #[must_use]
273 pub fn grouped(&self) -> bool {
274 self.grouped
275 }
276
277 #[must_use]
279 pub fn decimals(&self) -> usize {
280 self.decimals
281 }
282
283 #[must_use]
285 pub fn negatives(&self) -> Negatives {
286 self.negatives
287 }
288
289 #[must_use]
297 pub fn marks(&self) -> Vec<String> {
298 let mut marks = Vec::new();
299 if !self.opens_with.is_empty() {
300 marks.push(self.opens_with.clone());
301 }
302 if !self.closes_with.is_empty() {
303 marks.push(self.closes_with.clone());
304 }
305 if self.grouped {
306 marks.push(THOUSANDS_MARK.to_string());
307 }
308 if self.negatives == Negatives::Parentheses {
309 marks.push("(".to_owned());
310 marks.push(")".to_owned());
311 }
312 marks
313 }
314
315 #[must_use]
322 pub fn shape_pattern(&self) -> String {
323 let digits = if self.grouped { "[0-9][0-9,]*" } else { "[0-9]+" };
324 let fraction = if self.decimals > 0 { "([.][0-9]+)?" } else { "" };
325 let (open, close) = match self.negatives {
326 Negatives::Minus => ("-?", ""),
327 Negatives::Parentheses => ("[(]?", "[)]?"),
328 };
329 format!(
330 "^{open}{}{digits}{fraction}{}{close}$",
331 optional_literal(&self.opens_with),
332 optional_literal(&self.closes_with),
333 )
334 }
335}
336
337impl fmt::Display for NumberFormat {
338 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 f.write_str(&self.spelled)
340 }
341}
342
343fn optional_literal(text: &str) -> String {
349 if text.is_empty() {
350 return String::new();
351 }
352 let mut pattern = String::new();
353 for character in text.chars() {
354 if character.is_ascii_alphanumeric() {
355 pattern.push(character);
356 } else {
357 pattern.push('[');
358 pattern.push(character);
359 pattern.push(']');
360 }
361 }
362 format!("({pattern})?")
363}
364
365#[must_use]
371pub fn is_a_number(target: &DataType) -> bool {
372 matches!(target, DataType::Integer(_) | DataType::Decimal(_) | DataType::Float(_))
373}
374
375#[must_use]
387pub fn strip_marks(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> String {
388 let replace = spelling.lower.replace;
389 let prefix = spelling.lower.pattern_literal_prefix;
390 format.marks().iter().fold(normalize_trim(spelling, expr), |text, mark| {
391 format!("{replace}({text}, {prefix}'{}', {prefix}'')", mark.replace('\'', "''"))
392 })
393}
394
395fn separators_in_order(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> Option<String> {
408 if !format.grouped() {
409 return None;
410 }
411 let prefix = spelling.lower.pattern_literal_prefix;
412 Some(format!(
413 "{} NOT LIKE {prefix}'%{DECIMAL_POINT}%{THOUSANDS_MARK}%'",
414 normalize_trim(spelling, expr)
415 ))
416}
417
418fn bracket_sign(format: &NumberFormat, spelling: SidSpelling, expr: &str) -> Option<String> {
424 if format.negatives() != Negatives::Parentheses {
425 return None;
426 }
427 Some(format!("CASE WHEN {} LIKE '(%' THEN -1 ELSE 1 END", normalize_trim(spelling, expr)))
428}
429
430fn signed(reading: String, sign: Option<String>) -> String {
432 match sign {
433 None => reading,
434 Some(sign) => format!("({reading}) * ({sign})"),
435 }
436}
437
438#[must_use]
451pub fn read_with_safe_cast(
452 format: &NumberFormat,
453 spelling: SidSpelling,
454 expr: &str,
455 safe_cast: &dyn Fn(&str) -> String,
456) -> String {
457 let reading = safe_cast(&strip_marks(format, spelling, expr));
458 let reading = match separators_in_order(format, spelling, expr) {
459 None => reading,
460 Some(ordered) => format!("CASE WHEN {ordered} THEN {reading} END"),
461 };
462 signed(reading, bracket_sign(format, spelling, expr))
463}
464
465#[must_use]
477pub fn read_with_pattern_guard(
478 format: &NumberFormat,
479 spelling: SidSpelling,
480 expr: &str,
481 cast: &dyn Fn(&str) -> String,
482 matches: &dyn Fn(&str, &str) -> String,
483) -> String {
484 let trimmed = normalize_trim(spelling, expr);
485 let pattern = format!("'{}'", format.shape_pattern().replace('\'', "''"));
486 let reading = cast(&strip_marks(format, spelling, expr));
487 let guarded = format!("CASE WHEN {} THEN {reading} END", matches(&trimmed, &pattern));
490 signed(guarded, bracket_sign(format, spelling, expr))
491}
492
493#[must_use]
508pub fn read_with_digit_check(
509 format: &NumberFormat,
510 spelling: SidSpelling,
511 expr: &str,
512 cast: &dyn Fn(&str) -> String,
513) -> String {
514 let replace = spelling.lower.replace;
515 let prefix = spelling.lower.pattern_literal_prefix;
516 let stripped = strip_marks(format, spelling, expr);
517 let bare = (0..10u8).fold(stripped.clone(), |text, digit| {
518 format!("{replace}({text}, {prefix}'{digit}', {prefix}'')")
519 });
520 let reading = cast(&stripped);
521 let remainders = if format.decimals() > 0 {
526 format!("{prefix}'', {prefix}'.', {prefix}'-', {prefix}'-.'")
527 } else {
528 format!("{prefix}'', {prefix}'-'")
529 };
530 let mut conditions =
531 vec![format!("{stripped} <> {prefix}''"), format!("{bare} IN ({remainders})")];
532 conditions.extend(separators_in_order(format, spelling, expr));
533 let guarded = format!("CASE WHEN {} THEN {reading} END", conditions.join(" AND "));
534 signed(guarded, bracket_sign(format, spelling, expr))
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 #[test]
542 fn a_price_picture_says_a_symbol_thousands_and_two_places() {
543 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
544 assert_eq!(format.opens_with(), "$");
545 assert_eq!(format.closes_with(), "");
546 assert!(format.grouped());
547 assert_eq!(format.decimals(), 2);
548 assert_eq!(format.negatives(), Negatives::Minus);
549 assert_eq!(format.marks(), vec!["$".to_owned(), ",".to_owned()]);
550 }
551
552 #[test]
553 fn a_symbol_may_close_the_value_instead_of_opening_it() {
554 let format = NumberFormat::parse("#,##0.00 kr").expect("a lawful picture");
555 assert_eq!(format.opens_with(), "");
556 assert_eq!(format.closes_with(), " kr");
557 assert_eq!(format.marks(), vec![" kr".to_owned(), ",".to_owned()]);
558 }
559
560 #[test]
561 fn a_plain_picture_names_no_marks_at_all() {
562 let format = NumberFormat::parse("0.000").expect("a lawful picture");
563 assert!(!format.grouped());
564 assert_eq!(format.decimals(), 3);
565 assert!(format.marks().is_empty());
566 assert_eq!(format.shape_pattern(), "^-?[0-9]+([.][0-9]+)?$");
567 }
568
569 #[test]
570 fn a_ledger_negative_is_the_same_picture_in_brackets() {
571 let format = NumberFormat::parse("$#,##0.00;($#,##0.00)").expect("a lawful picture");
572 assert_eq!(format.negatives(), Negatives::Parentheses);
573 assert_eq!(
574 format.marks(),
575 vec!["$".to_owned(), ",".to_owned(), "(".to_owned(), ")".to_owned()]
576 );
577 assert_eq!(format.spelled(), "$#,##0.00;($#,##0.00)");
578 }
579
580 #[test]
581 fn the_shape_holds_the_symbol_and_the_groups_where_the_picture_put_them() {
582 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
583 assert_eq!(format.shape_pattern(), "^-?([$])?[0-9][0-9,]*([.][0-9]+)?$");
584 }
585
586 #[test]
587 fn the_reading_holds_the_thousands_mark_in_front_of_the_decimal_point() {
588 let format = NumberFormat::parse("$#,##0.00").expect("a lawful picture");
589 let read = read_with_safe_cast(&format, crate::ANSI_SID_SPELLING, "v", &|text| {
590 format!("TRY_CAST({text} AS DECIMAL(12, 2))")
591 });
592 assert!(read.contains("TRIM(v) NOT LIKE '%.%,%'"), "{read}");
595 }
596
597 #[test]
598 fn a_picture_with_no_thousands_mark_needs_no_such_condition() {
599 let format = NumberFormat::parse("0.00").expect("a lawful picture");
600 let read = read_with_safe_cast(&format, crate::ANSI_SID_SPELLING, "v", &|text| {
601 format!("TRY_CAST({text} AS DECIMAL(12, 2))")
602 });
603 assert_eq!(
604 read, "TRY_CAST(TRIM(v) AS DECIMAL(12, 2))",
605 "nothing to take out, nothing to hold"
606 );
607 }
608
609 #[test]
610 fn a_whole_numbers_digit_check_leaves_no_room_for_a_decimal_point() {
611 let whole = NumberFormat::parse("#0").expect("a lawful picture");
612 let read = read_with_digit_check(&whole, crate::ANSI_SID_SPELLING, "v", &|text| {
613 format!("CAST({text} AS INTEGER)")
614 });
615 assert!(read.contains("IN ('', '-')"), "{read}");
618 let fractional = NumberFormat::parse("#0.0").expect("a lawful picture");
619 let read = read_with_digit_check(&fractional, crate::ANSI_SID_SPELLING, "v", &|text| {
620 format!("CAST({text} AS DECIMAL(18, 6))")
621 });
622 assert!(read.contains("IN ('', '.', '-', '-.')"), "{read}");
623 }
624
625 #[test]
626 fn a_bracketed_negative_is_read_as_a_sign_rather_than_a_mark() {
627 let format = NumberFormat::parse("$#,##0.00;($#,##0.00)").expect("a lawful picture");
628 let read = read_with_safe_cast(&format, crate::ANSI_SID_SPELLING, "v", &|text| {
629 format!("TRY_CAST({text} AS DECIMAL(12, 2))")
630 });
631 assert!(read.contains("CASE WHEN TRIM(v) LIKE '(%' THEN -1 ELSE 1 END"), "{read}");
632 assert!(read.contains("REPLACE(REPLACE(REPLACE(REPLACE(TRIM(v)"), "{read}");
633 }
634
635 #[test]
636 fn a_picture_with_no_digits_is_refused() {
637 assert_eq!(NumberFormat::parse("$"), Err(NumberFormatError::NoDigits));
638 assert_eq!(NumberFormat::parse(""), Err(NumberFormatError::Empty));
639 }
640
641 #[test]
642 fn a_source_writing_the_separators_the_other_way_round_is_refused_by_name() {
643 let refused = NumberFormat::parse("#.##0,00").expect_err("not covered");
644 assert_eq!(refused, NumberFormatError::MarkAfterPoint);
645 assert!(refused.to_string().contains("2.899,99"), "{refused}");
646 }
647
648 #[test]
649 fn a_thousands_mark_separates_groups_of_three() {
650 assert_eq!(NumberFormat::parse("#,##0,0"), Err(NumberFormatError::GroupIsNotThree(1)));
651 }
652
653 #[test]
654 fn two_decimal_points_are_refused() {
655 assert_eq!(NumberFormat::parse("0.0.0"), Err(NumberFormatError::TwoPoints));
656 }
657
658 #[test]
659 fn digits_on_both_sides_of_the_symbol_are_refused() {
660 assert_eq!(NumberFormat::parse("0 kr 0"), Err(NumberFormatError::DigitsAfterSymbol));
661 }
662
663 #[test]
664 fn a_second_picture_that_is_not_the_first_in_brackets_is_refused() {
665 assert_eq!(
666 NumberFormat::parse("$#,##0.00;-$#,##0.00"),
667 Err(NumberFormatError::NegativeIsNotBracketed)
668 );
669 assert_eq!(NumberFormat::parse("0;(0);0"), Err(NumberFormatError::TooManyPictures));
670 }
671}