1use std::collections::HashMap;
9use std::sync::LazyLock;
10
11use crate::date::excel_serial_to_datetime;
12
13pub static BUILT_IN_NUM_FMT: LazyLock<HashMap<i32, &'static str>> = LazyLock::new(|| {
18 let mut m = HashMap::new();
19 m.insert(0, "General");
20 m.insert(1, "0");
21 m.insert(2, "0.00");
22 m.insert(3, "#,##0");
23 m.insert(4, "#,##0.00");
24 m.insert(9, "0%");
25 m.insert(10, "0.00%");
26 m.insert(11, "0.00E+00");
27 m.insert(12, "# ?/?");
28 m.insert(13, "# ??/??");
29 m.insert(14, "mm-dd-yy");
30 m.insert(15, "d-mmm-yy");
31 m.insert(16, "d-mmm");
32 m.insert(17, "mmm-yy");
33 m.insert(18, "h:mm AM/PM");
34 m.insert(19, "h:mm:ss AM/PM");
35 m.insert(20, "hh:mm");
36 m.insert(21, "hh:mm:ss");
37 m.insert(22, "m/d/yy hh:mm");
38 m.insert(37, "#,##0 ;(#,##0)");
39 m.insert(38, "#,##0 ;[Red](#,##0)");
40 m.insert(39, "#,##0.00 ;(#,##0.00)");
41 m.insert(40, "#,##0.00 ;[Red](#,##0.00)");
42 m.insert(45, "mm:ss");
43 m.insert(46, "[h]:mm:ss");
44 m.insert(47, "mm:ss.0");
45 m.insert(48, "##0.0E+0");
46 m.insert(49, "@");
47 m
48});
49
50pub fn built_in_num_fmt_code(num_fmt_id: i32) -> Option<&'static str> {
52 BUILT_IN_NUM_FMT.get(&num_fmt_id).copied()
53}
54
55pub fn apply_number_format(
66 value: f64,
67 num_fmt_id: i32,
68 format_code: Option<&str>,
69 date1904: bool,
70) -> String {
71 let code = format_code
72 .filter(|c| !c.is_empty())
73 .or_else(|| built_in_num_fmt_code(num_fmt_id))
74 .unwrap_or("General");
75
76 apply_format_code(value, code, date1904)
77}
78
79pub fn format_number(value: f64, code: &str, date1904: bool) -> String {
84 apply_format_code(value, code, date1904)
85}
86
87fn apply_format_code(value: f64, code: &str, date1904: bool) -> String {
88 let code = code.trim();
89 if code.eq_ignore_ascii_case("General") || code.is_empty() {
90 return format_general(value);
91 }
92
93 let (section, is_negative_section, switch_argument, locale_code) =
98 choose_format_section(code, value);
99 let abs_value = value.abs();
100
101 let mut result = if is_date_format(§ion) {
102 if let Ok(dt) = excel_serial_to_datetime(abs_value, date1904) {
103 format_date_time(§ion, abs_value, dt, locale_code.as_deref())
104 } else {
105 abs_value.to_string()
106 }
107 } else {
108 format_numeric(§ion, abs_value)
109 };
110
111 if !is_negative_section && value < 0.0 && result != "0" {
115 result = format!("-{}", result);
116 }
117
118 if let Some(sw) = switch_argument {
120 result = apply_switch_argument(&result, &sw);
121 }
122
123 result
124}
125
126fn apply_switch_argument(result: &str, sw: &str) -> String {
127 match sw.to_ascii_uppercase().as_str() {
128 "DBNUM1" => result
129 .chars()
130 .map(|c| match c {
131 '0' => '\u{25CB}',
132 '1' => '\u{4E00}',
133 '2' => '\u{4E8C}',
134 '3' => '\u{4E09}',
135 '4' => '\u{56DB}',
136 '5' => '\u{4E94}',
137 '6' => '\u{516D}',
138 '7' => '\u{4E03}',
139 '8' => '\u{516B}',
140 '9' => '\u{4E5D}',
141 _ => c,
142 })
143 .collect(),
144 "DBNUM2" => result
145 .chars()
146 .map(|c| match c {
147 '0' => '\u{96F6}',
148 '1' => '\u{58F9}',
149 '2' => '\u{8D30}',
150 '3' => '\u{53C1}',
151 '4' => '\u{8086}',
152 '5' => '\u{4F0D}',
153 '6' => '\u{9646}',
154 '7' => '\u{67D2}',
155 '8' => '\u{634C}',
156 '9' => '\u{7396}',
157 _ => c,
158 })
159 .collect(),
160 "DBNUM3" => result
161 .chars()
162 .map(|c| match c {
163 '0' => '\u{FF10}',
164 '1' => '\u{FF11}',
165 '2' => '\u{FF12}',
166 '3' => '\u{FF13}',
167 '4' => '\u{FF14}',
168 '5' => '\u{FF15}',
169 '6' => '\u{FF16}',
170 '7' => '\u{FF17}',
171 '8' => '\u{FF18}',
172 '9' => '\u{FF19}',
173 _ => c,
174 })
175 .collect(),
176 _ => result.to_string(),
177 }
178}
179
180fn format_general(value: f64) -> String {
181 if value.fract() == 0.0 && value.abs() < 1e15 {
182 format!("{:.0}", value)
183 } else {
184 format!("{}", value)
185 }
186}
187
188fn is_date_format(code: &str) -> bool {
190 let upper = code.to_ascii_uppercase();
192 if upper.contains("E+0") || upper.contains("E-0") {
193 return false;
194 }
195 let mut chars = code.chars().peekable();
201 while let Some(c) = chars.next() {
202 match c {
203 '"' => {
204 for c in chars.by_ref() {
205 if c == '"' {
206 break;
207 }
208 }
209 }
210 '\\' => {
211 chars.next();
212 }
213 '[' => {
214 let mut content = String::new();
215 for c in chars.by_ref() {
216 if c == ']' {
217 break;
218 }
219 content.push(c);
220 }
221 if matches!(content.to_ascii_lowercase().as_str(), "h" | "m" | "s") {
222 return true;
223 }
224 }
225 'y' | 'Y' | 'd' | 'D' | 'h' | 'H' | 's' | 'S' | 'm' | 'M' | 'e' | 'E' | 'g' | 'G' => {
226 return true;
227 }
228 _ => {}
229 }
230 }
231 false
232}
233
234fn split_format_sections(code: &str) -> Vec<String> {
241 let mut sections: Vec<String> = vec![String::new()];
242 let mut in_quote = false;
243 let mut chars = code.chars().peekable();
244
245 while let Some(ch) = chars.next() {
246 if ch == '"' {
247 in_quote = !in_quote;
248 sections.last_mut().unwrap().push(ch);
249 } else if ch == '\\' && !in_quote {
250 sections.last_mut().unwrap().push(ch);
251 if let Some(next) = chars.next() {
252 sections.last_mut().unwrap().push(next);
253 }
254 } else if ch == ';' && !in_quote {
255 sections.push(String::new());
256 } else {
257 sections.last_mut().unwrap().push(ch);
258 }
259 }
260
261 sections
262}
263
264#[derive(Debug, Clone, PartialEq)]
266enum FormatCondition {
267 Greater(f64),
268 GreaterOrEqual(f64),
269 Less(f64),
270 LessOrEqual(f64),
271 Equal(f64),
272 NotEqual(f64),
273}
274
275impl FormatCondition {
276 fn matches(&self, value: f64) -> bool {
278 match self {
279 FormatCondition::Greater(threshold) => value > *threshold,
280 FormatCondition::GreaterOrEqual(threshold) => value >= *threshold,
281 FormatCondition::Less(threshold) => value < *threshold,
282 FormatCondition::LessOrEqual(threshold) => value <= *threshold,
283 FormatCondition::Equal(threshold) => value == *threshold,
284 FormatCondition::NotEqual(threshold) => value != *threshold,
285 }
286 }
287}
288
289fn parse_condition(section: &str) -> (Option<FormatCondition>, &str) {
291 let Some(rest) = section.strip_prefix('[') else {
292 return (None, section);
293 };
294 let Some(end) = rest.find(']') else {
295 return (None, section);
296 };
297 let token = &rest[..end];
298 let after = &rest[end + 1..];
299
300 let op_and_value = if token.starts_with(">=") {
301 Some((">=", &token[2..]))
302 } else if token.starts_with("<>") {
303 Some(("<>", &token[2..]))
305 } else if token.starts_with("<=") {
306 Some(("<=", &token[2..]))
307 } else if token.starts_with('<') {
308 Some(("<", &token[1..]))
309 } else if token.starts_with('>') {
310 Some((">", &token[1..]))
311 } else if token.starts_with('=') {
312 Some(("=", &token[1..]))
313 } else {
314 None
315 };
316
317 if let Some((op, val_str)) = op_and_value {
318 let val_str = val_str.trim();
319 if let Ok(value) = val_str.parse::<f64>() {
320 let condition = match op {
321 ">" => FormatCondition::Greater(value),
322 ">=" => FormatCondition::GreaterOrEqual(value),
323 "<" => FormatCondition::Less(value),
324 "<=" => FormatCondition::LessOrEqual(value),
325 "=" => FormatCondition::Equal(value),
326 "<>" => FormatCondition::NotEqual(value),
327 _ => return (None, section),
328 };
329 return (Some(condition), after);
330 }
331 }
332
333 (None, section)
334}
335
336fn is_color_token(token: &str) -> bool {
338 const NAMED_COLORS: &[&str] = &[
339 "Black", "Blue", "Cyan", "Green", "Magenta", "Red", "White", "Yellow",
340 ];
341 if NAMED_COLORS
342 .iter()
343 .any(|c| c.eq_ignore_ascii_case(token))
344 {
345 return true;
346 }
347 if token.len() >= 6 && token[..5].eq_ignore_ascii_case("Color") {
349 token[5..].chars().all(|c| c.is_ascii_digit())
350 } else {
351 false
352 }
353}
354
355fn is_currency_language_token(token: &str) -> bool {
359 if token.contains('$')
361 || token.contains('€')
362 || token.contains('£')
363 || token.contains('¥')
364 {
365 return true;
366 }
367 if token.starts_with('-') && token[1..].chars().all(|c| c.is_ascii_hexdigit()) {
369 return true;
370 }
371 matches!(token.to_ascii_lowercase().as_str(), "f800" | "f400" | "x-sysdate" | "x-systime" | "1010000")
373}
374
375fn extract_currency_symbol(token: &str) -> Option<&str> {
379 if token == "$$" {
381 return Some("$");
382 }
383 for sym in &["€", "£", "¥"] {
386 if let Some(pos) = token.find(sym) {
387 let len = sym.len();
388 return Some(&token[pos..pos + len]);
389 }
390 }
391 None
393}
394
395fn extract_switch_argument(token: &str) -> Option<String> {
397 if token.len() >= 6 && token[..5].eq_ignore_ascii_case("DBNum") {
398 if token[5..].chars().all(|c| c.is_ascii_digit()) {
399 return Some(token.to_string());
400 }
401 }
402 None
403}
404
405fn extract_locale_code(token: &str) -> Option<String> {
408 if token.starts_with('-') {
409 return Some(token[1..].to_string());
410 }
411 if let Some(pos) = token.find('-') {
412 return Some(token[pos + 1..].to_string());
413 }
414 None
415}
416
417fn strip_section_metadata(section: &str) -> (Option<FormatCondition>, String, Option<String>, Option<String>) {
420 let mut section = section;
421 let mut condition: Option<FormatCondition> = None;
422 let mut currency_prefix = String::new();
423 let mut switch_argument: Option<String> = None;
424 let mut locale_code: Option<String> = None;
425
426 while let Some(rest) = section.strip_prefix('[') {
427 let Some(end) = rest.find(']') else { break };
428 let token = &rest[..end];
429
430 if condition.is_none() {
433 let (cond, after) = parse_condition(section);
434 if let Some(c) = cond {
435 condition = Some(c);
436 section = after;
437 continue;
438 }
439 }
440
441 if is_color_token(token) {
442 section = &rest[end + 1..];
443 continue;
444 }
445
446 if is_currency_language_token(token) {
447 if let Some(sym) = extract_currency_symbol(token) {
448 currency_prefix.push_str(sym);
449 }
450 if let Some(code) = extract_locale_code(token) {
451 locale_code = Some(code);
452 }
453 section = &rest[end + 1..];
454 continue;
455 }
456
457 if let Some(sw) = extract_switch_argument(token) {
458 switch_argument = Some(sw);
459 section = &rest[end + 1..];
460 continue;
461 }
462
463 break;
466 }
467
468 let mut body = currency_prefix;
469 body.push_str(section);
470 (condition, body, switch_argument, locale_code)
471}
472
473fn choose_format_section(code: &str, value: f64) -> (String, bool, Option<String>, Option<String>) {
481 let sections = split_format_sections(code);
482 let parsed: Vec<(Option<FormatCondition>, String, Option<String>, Option<String>)> =
483 sections.iter().map(|s| strip_section_metadata(s)).collect();
484
485 let selected_idx = if value >= 0.0 {
486 0
487 } else if parsed.len() >= 2 {
488 1
489 } else {
490 0
491 };
492 let is_negative = value < 0.0 && selected_idx == 1;
493 (parsed[selected_idx].1.clone(), is_negative, parsed[selected_idx].2.clone(), parsed[selected_idx].3.clone())
494}
495
496pub fn is_date_time_pattern(pattern: &str) -> bool {
506 if pattern.is_empty() {
507 return true;
508 }
509 for section in pattern.split(';') {
510 if !is_date_time_section(section) {
511 return false;
512 }
513 }
514 true
515}
516
517fn is_date_time_section(section: &str) -> bool {
518 let mut chars = section.chars().peekable();
519 let mut prev: Option<char> = None;
520
521 while let Some(ch) = chars.next() {
522 match ch {
523 '"' => {
524 if !consume_quoted_literal(&mut chars) {
525 return false;
526 }
527 prev = Some('"');
528 }
529 '\\' => {
530 if chars.next().is_none() {
531 return false;
532 }
533 prev = Some('\\');
534 }
535 '[' => {
536 if !consume_bracketed_expr(&mut chars) {
537 return false;
538 }
539 prev = Some(']');
540 }
541 '@' | '0' | '#' | '?' | '%' => {
542 return false;
543 }
544 '/' => {
545 if prev.map_or(false, is_numeric_placeholder) {
550 return false;
551 }
552 if chars.peek().copied().map_or(false, is_numeric_placeholder) {
553 return false;
554 }
555 prev = Some('/');
556 }
557 'e' | 'E' => {
558 if let Some(&next) = chars.peek() {
560 if next == '+' || next == '-' {
561 let mut tmp = chars.clone();
562 tmp.next();
563 if tmp.peek().copied().map_or(false, is_numeric_placeholder) {
564 return false;
565 }
566 }
567 }
568 prev = Some(ch);
569 }
570 _ => {
571 prev = Some(ch);
572 }
573 }
574 }
575 true
576}
577
578fn consume_quoted_literal(chars: &mut std::iter::Peekable<impl Iterator<Item = char>>) -> bool {
579 for c in chars {
580 if c == '"' {
581 return true;
582 }
583 }
584 false
585}
586
587fn consume_bracketed_expr(chars: &mut std::iter::Peekable<impl Iterator<Item = char>>) -> bool {
588 for c in chars {
589 if c == ']' {
590 return true;
591 }
592 }
593 false
594}
595
596fn is_numeric_placeholder(c: char) -> bool {
597 matches!(c, '0' | '#' | '?')
598}
599
600fn strip_scaling_commas(code: &str) -> (usize, String) {
608 let mut last_placeholder: Option<usize> = None;
609 let mut chars = code.char_indices().peekable();
610 while let Some((i, c)) = chars.next() {
611 match c {
612 '"' => {
613 for (_, c) in chars.by_ref() {
614 if c == '"' {
615 break;
616 }
617 }
618 }
619 '\\' => {
620 chars.next();
621 }
622 '0' | '#' | '?' => last_placeholder = Some(i),
623 _ => {}
624 }
625 }
626 let Some(last) = last_placeholder else {
627 return (0, code.to_string());
628 };
629 let mut factor = 0;
630 let mut result = String::with_capacity(code.len());
631 let mut in_quote = false;
632 let mut escaped = false;
633 for (i, c) in code.chars().enumerate() {
634 if escaped {
635 result.push(c);
636 escaped = false;
637 continue;
638 }
639 match c {
640 '\\' => {
641 result.push(c);
642 escaped = true;
643 }
644 '"' => {
645 result.push(c);
646 in_quote = !in_quote;
647 }
648 ',' if !in_quote && i > last => factor += 1,
649 _ => result.push(c),
650 }
651 }
652 (factor, result)
653}
654
655fn analysis_pattern(code: &str) -> String {
659 let mut out = String::with_capacity(code.len());
660 let mut chars = code.chars().peekable();
661 while let Some(c) = chars.next() {
662 match c {
663 '"' => {
664 for c in chars.by_ref() {
665 if c == '"' {
666 break;
667 }
668 }
669 }
670 '\\' | '_' | '*' => {
671 chars.next();
672 }
673 _ => out.push(c),
674 }
675 }
676 out
677}
678
679fn format_numeric(code: &str, value: f64) -> String {
680 let (scaling, code) = strip_scaling_commas(code);
681 let code = code.as_str();
682 let value = value / 1000f64.powi(scaling as i32);
683 let analysis = analysis_pattern(code);
684
685 let percent_count = analysis.chars().filter(|&c| c == '%').count();
687 let base_value = value * (100.0_f64.powi(percent_count as i32));
688
689 if analysis.to_ascii_uppercase().contains("E+0")
691 || analysis.to_ascii_uppercase().contains("E-0")
692 {
693 return format_scientific(&analysis, base_value);
694 }
695
696 if analysis.contains('/') {
698 return format_fraction(code, base_value);
699 }
700
701 let has_placeholders = analysis.chars().any(|c| matches!(c, '0' | '#' | '?'));
703 if !has_placeholders {
704 return merge_literals(code, "", false);
705 }
706
707 let parts: Vec<&str> = analysis.split('.').collect();
709 let int_pattern = parts.first().copied().unwrap_or("");
710 let frac_pattern = parts.get(1).copied().unwrap_or("");
711
712 let use_thousands = int_pattern.contains(',');
713 let frac_digits = count_digit_placeholders(frac_pattern);
714 let force_decimal = frac_pattern.contains('0');
715
716 let rounded = round_to_digits(base_value, frac_digits);
717
718 let mut int_part = rounded.trunc().abs() as i64;
719 if (rounded.fract().abs() - 1.0).abs() < 1e-12 {
721 int_part += 1;
722 }
723
724 let mut result = String::new();
725 result.push_str(&format_integer(int_part, int_pattern));
726
727 if force_decimal || (!frac_pattern.is_empty() && frac_digits > 0) {
728 let frac = rounded.fract().abs();
729 let frac_str = format_fractional(frac, frac_digits, frac_pattern.contains('0'));
730 if !frac_str.is_empty() {
731 result.push('.');
732 result.push_str(&frac_str);
733 } else if force_decimal {
734 result.push('.');
735 result.push_str(&"0".repeat(frac_digits.max(1)));
736 }
737 }
738
739 if use_thousands {
740 result = add_thousands(result);
741 }
742
743 result = merge_literals(code, &result, true);
745
746 if percent_count > 0 {
747 result.push('%');
748 }
749
750 result
751}
752
753fn count_digit_placeholders(pattern: &str) -> usize {
754 pattern
755 .chars()
756 .filter(|&c| c == '0' || c == '#' || c == '?')
757 .count()
758}
759
760fn format_integer(value: i64, pattern: &str) -> String {
761 let min_digits = pattern.chars().filter(|&c| c == '0').count();
762 let has_question = pattern.contains('?');
763 let digits_needed = pattern
764 .chars()
765 .filter(|&c| c == '#' || c == '0' || c == '?')
766 .count();
767
768 let mut s = value.to_string();
769 if s.len() < min_digits {
770 s = format!("{}{}", "0".repeat(min_digits - s.len()), s);
771 }
772 if has_question && s.len() < digits_needed {
775 s = format!("{}{}", " ".repeat(digits_needed - s.len()), s);
776 }
777 s
778}
779
780fn format_fractional(frac: f64, digits: usize, force_zero: bool) -> String {
781 if digits == 0 {
782 return String::new();
783 }
784 let scaled = (frac * 10.0_f64.powi(digits as i32)).round() as i64;
785 if scaled == 0 && !force_zero {
786 return String::new();
787 }
788 let mut s = format!("{:0digits$}", scaled, digits = digits);
789 if !force_zero {
790 s = s.trim_end_matches('0').to_string();
791 }
792 s
793}
794
795fn add_thousands(text: String) -> String {
796 let mut result = String::new();
797 let (int_part, frac_part) = if let Some(pos) = text.find('.') {
798 (&text[..pos], Some(&text[pos..]))
799 } else {
800 (text.as_str(), None)
801 };
802
803 let (sign, digits) = if int_part.starts_with('-') {
804 ("-", &int_part[1..])
805 } else {
806 ("", int_part)
807 };
808
809 result.push_str(sign);
810 for (i, ch) in digits.chars().enumerate() {
811 if i > 0 && (digits.len() - i) % 3 == 0 {
812 result.push(',');
813 }
814 result.push(ch);
815 }
816
817 if let Some(frac) = frac_part {
818 result.push_str(frac);
819 }
820 result
821}
822
823fn format_scientific(code: &str, value: f64) -> String {
824 let upper = code.to_ascii_uppercase();
825 let mantissa_code = upper.split('E').next().unwrap_or("");
826 let frac_digits = mantissa_code
827 .split('.')
828 .nth(1)
829 .map(|s| s.chars().filter(|&c| c == '0' || c == '#').count())
830 .unwrap_or(0);
831
832 if value == 0.0 {
833 let mantissa = format!("{:.*}", frac_digits, 0.0);
834 return format!("{}E+00", mantissa);
835 }
836
837 let sign = if value < 0.0 { "-" } else { "" };
838 let abs = value.abs();
839 let exponent = abs.log10().floor() as i32;
840 let mantissa = abs / 10.0_f64.powi(exponent);
841 format!("{}{:.*}E{:+03}", sign, frac_digits, mantissa, exponent)
842}
843
844fn format_fraction(code: &str, value: f64) -> String {
845 let analysis = analysis_pattern(code);
846 let Some(slash) = analysis.find('/') else {
847 return merge_literals(code, &value.to_string(), true);
848 };
849 let before = &analysis[..slash];
850 let after = &analysis[slash + 1..];
851
852 let num_width = before.chars().rev().take_while(|&c| c == '?').count();
854 let den_token: String = after
856 .chars()
857 .take_while(|&c| c == '?' || c.is_ascii_digit())
858 .collect();
859
860 let abs = value.abs();
861 let whole = abs.trunc() as i64;
862 let frac = abs.fract();
863
864 let int_region = &before[..before.len() - num_width];
868 let int_width = int_region
869 .chars()
870 .rev()
871 .take_while(|&c| matches!(c, '0' | '#' | '?'))
872 .count();
873 let int_pattern = &int_region[int_region.len() - int_width..];
874 let between = &int_region[..int_region.len() - int_width];
875
876 let mut result = String::new();
877 if int_width > 0 {
878 result.push_str(&format_integer(whole, int_pattern));
879 }
880 result.push_str(between);
881
882 if den_token.chars().all(|c| c == '?') && !den_token.is_empty() {
883 result.push_str(&float_to_fraction(frac, num_width, den_token.len()));
884 } else if let Ok(denom) = den_token.parse::<f64>() {
885 let num = (frac * denom).round() as i64;
887 result.push_str(&format!("{}/{}", num, denom as i64));
888 }
889 result
890}
891
892fn float_to_fraction(x: f64, numerator_place_holder: usize, denominator_place_holder: usize) -> String {
896 if denominator_place_holder == 0 {
897 return String::new();
898 }
899 let limit = 10i64.pow(denominator_place_holder as u32);
900 let (num, den) = float_to_frac_continued(x, limit);
901 if num == 0 {
902 return " ".repeat(numerator_place_holder + denominator_place_holder + 1);
903 }
904 let num_str = num.to_string();
905 let den_str = den.to_string();
906 let num_pad = numerator_place_holder.saturating_sub(num_str.len());
907 let den_pad = denominator_place_holder.saturating_sub(den_str.len());
908 format!(
909 "{}{}/{}{}",
910 " ".repeat(num_pad),
911 num_str,
912 den_str,
913 " ".repeat(den_pad)
914 )
915}
916
917fn float_to_frac_continued(mut r: f64, denominator_limit: i64) -> (i64, i64) {
920 let mut p1: i64 = 1;
921 let mut q1: i64 = 0;
922 let mut p2: i64 = 0;
923 let mut q2: i64 = 1;
924 let mut lasta: i64 = 0;
925 let mut lastb: i64 = 0;
926 loop {
927 let a = r.floor() as i64;
928 let curra = a * p1 + p2;
929 let currb = a * q1 + q2;
930 p2 = p1;
931 q2 = q1;
932 p1 = curra;
933 q1 = currb;
934 let frac = r - a as f64;
935 if q1 >= denominator_limit {
936 return (lasta, lastb);
937 }
938 if frac.abs() < 1e-12 {
939 return (curra, currb);
940 }
941 lasta = curra;
942 lastb = currb;
943 r = 1.0 / frac;
944 }
945}
946
947fn merge_literals(code: &str, formatted_number: &str, append_number: bool) -> String {
948 let mut result = String::new();
951 let mut digit_run_seen = false;
952 let mut chars = code.chars().peekable();
953
954 while let Some(ch) = chars.next() {
955 if ch == '"' {
956 while let Some(c) = chars.next() {
958 if c == '"' {
959 break;
960 }
961 result.push(c);
962 }
963 continue;
964 }
965 if ch == '\\' {
966 if let Some(c) = chars.next() {
967 result.push(c);
968 }
969 continue;
970 }
971 if ch == '_' {
972 if chars.next().is_some() {
975 result.push(' ');
976 }
977 continue;
978 }
979 if ch == '*' {
980 chars.next();
982 continue;
983 }
984 if ch == '(' || ch == ')' || ch == '[' || ch == ']' {
985 result.push(ch);
986 continue;
987 }
988 if ch == '%' {
989 continue;
991 }
992 if matches!(ch, '$' | '€' | '£' | '¥') {
993 result.push(ch);
994 continue;
995 }
996 if ch == '-' {
997 result.push('-');
1000 continue;
1001 }
1002 if ch.is_ascii_digit() || ch == '#' || ch == '?' || ch == ',' || ch == '.' {
1003 if !digit_run_seen {
1004 result.push_str(formatted_number);
1005 digit_run_seen = true;
1006 }
1007 continue;
1008 }
1009 result.push(ch);
1010 }
1011
1012 if !digit_run_seen && append_number {
1013 result.push_str(formatted_number);
1014 }
1015
1016 result
1017}
1018
1019fn round_to_digits(value: f64, digits: usize) -> f64 {
1020 if digits == 0 {
1021 return value.round();
1022 }
1023 let factor = 10.0_f64.powi(digits as i32);
1024 (value * factor).round() / factor
1025}
1026
1027fn fraction_to_time(fraction: f64) -> chrono::NaiveTime {
1032 let total_seconds = (fraction.abs().fract() * 24.0 * 60.0 * 60.0).round() as u32;
1033 let hours = (total_seconds / 3600).min(23);
1034 let minutes = ((total_seconds % 3600) / 60).min(59);
1035 let seconds = (total_seconds % 60).min(59);
1036 chrono::NaiveTime::from_hms_opt(hours, minutes, seconds).unwrap_or(chrono::NaiveTime::MIN)
1037}
1038
1039fn format_date_time(
1040 code: &str,
1041 value: f64,
1042 dt: chrono::NaiveDateTime,
1043 locale_code: Option<&str>,
1044) -> String {
1045 use chrono::{Datelike, Timelike};
1046
1047 let mut result = String::new();
1048 let mut chars = code.chars().peekable();
1049 let mut prev_token_was_hour = false;
1050 let mut use_elapsed_hour = false;
1051 let mut current_dt = dt;
1052
1053 while let Some(ch) = chars.next() {
1054 if ch == '[' {
1055 let mut bracket = String::new();
1056 while let Some(c) = chars.next() {
1057 if c == ']' {
1058 break;
1059 }
1060 bracket.push(c);
1061 }
1062 match bracket.to_ascii_uppercase().as_str() {
1063 "H" => {
1064 let total_hours = (value * 24.0).floor();
1065 result.push_str(&format!("{:.0}", total_hours));
1066 let remaining = value - total_hours / 24.0;
1067 current_dt = dt.date().and_time(fraction_to_time(remaining));
1068 use_elapsed_hour = true;
1069 }
1070 "M" => result.push_str(&(value * 24.0 * 60.0).floor().to_string()),
1071 "S" => result.push_str(&(value * 24.0 * 60.0 * 60.0).floor().to_string()),
1072 _ => {
1073 result.push('[');
1074 result.push_str(&bracket);
1075 result.push(']');
1076 }
1077 }
1078 prev_token_was_hour = false;
1079 continue;
1080 }
1081 if ch == '"' {
1082 while let Some(c) = chars.next() {
1083 if c == '"' {
1084 break;
1085 }
1086 result.push(c);
1087 }
1088 continue;
1089 }
1090 if ch == '\\' {
1091 if let Some(c) = chars.next() {
1092 result.push(c);
1093 }
1094 continue;
1095 }
1096
1097 let upper = ch.to_ascii_uppercase();
1098 if upper == 'Y' {
1099 let count = 1 + count_repeats(&mut chars, 'y', 'Y');
1100 let year = dt.year();
1101 if count >= 4 {
1102 result.push_str(&format!("{:04}", year));
1103 } else {
1104 result.push_str(&format!("{:02}", year % 100));
1105 }
1106 prev_token_was_hour = false;
1107 } else if upper == 'M' {
1108 let count = 1 + count_repeats(&mut chars, 'm', 'M');
1109 let dt = if use_elapsed_hour { current_dt } else { dt };
1110 if prev_token_was_hour || use_elapsed_hour {
1111 result.push_str(&format!("{:02}", dt.minute()));
1113 } else {
1114 match count {
1116 1 => result.push_str(&dt.month().to_string()),
1117 2 => result.push_str(&format!("{:02}", dt.month())),
1118 3 => result.push_str(short_month_name(dt.month(), locale_code)),
1119 4 => result.push_str(long_month_name(dt.month(), locale_code)),
1120 5 => result.push(short_month_name(dt.month(), locale_code).chars().next().unwrap()),
1121 _ => result.push_str(short_month_name(dt.month(), locale_code)),
1122 }
1123 }
1124 prev_token_was_hour = false;
1125 } else if upper == 'D' {
1126 let count = 1 + count_repeats(&mut chars, 'd', 'D');
1127 match count {
1128 1 => result.push_str(&dt.day().to_string()),
1129 2 => result.push_str(&format!("{:02}", dt.day())),
1130 3 => result.push_str(short_weekday_name(dt.weekday(), locale_code)),
1131 4 => result.push_str(long_weekday_name(dt.weekday(), locale_code)),
1132 _ => result.push_str(&format!("{:02}", dt.day())),
1133 }
1134 prev_token_was_hour = false;
1135 } else if upper == 'H' {
1136 let count = 1 + count_repeats(&mut chars, 'h', 'H');
1137 let dt = if use_elapsed_hour { current_dt } else { dt };
1138 let hour24 = dt.hour();
1139 let hour12 = if hour24 == 0 {
1140 12
1141 } else if hour24 > 12 {
1142 hour24 - 12
1143 } else {
1144 hour24
1145 };
1146 if count >= 2 {
1147 result.push_str(&format!("{:02}", hour24));
1148 } else {
1149 result.push_str(&hour12.to_string());
1150 }
1151 prev_token_was_hour = true;
1152 } else if upper == 'S' {
1153 let count = 1 + count_repeats(&mut chars, 's', 'S');
1154 let dt = if use_elapsed_hour { current_dt } else { dt };
1155 if count >= 2 {
1156 result.push_str(&format!("{:02}", dt.second()));
1157 } else {
1158 result.push_str(&dt.second().to_string());
1159 }
1160 prev_token_was_hour = false;
1161 } else if upper == 'A' {
1162 let ahead: String = chars.clone().take(4).collect();
1164 let ahead_upper = ahead.to_ascii_uppercase();
1165 if ahead_upper.starts_with("M/PM") {
1166 result.push_str(if dt.hour() < 12 { "AM" } else { "PM" });
1167 for _ in 0..4 {
1168 chars.next();
1169 }
1170 } else if ahead_upper.starts_with("/P") {
1171 result.push(if dt.hour() < 12 { 'A' } else { 'P' });
1172 chars.next(); chars.next(); } else {
1175 result.push(ch);
1176 }
1177 prev_token_was_hour = false;
1178 } else if ch == '0' {
1179 let count = 1 + count_repeats(&mut chars, '0', '0').min(2);
1181 let ms = dt.nanosecond() / 1_000_000;
1182 result.push_str(&format!("{:03}", ms)[..count]);
1183 prev_token_was_hour = false;
1184 } else {
1185 result.push(ch);
1186 if ch != ':' && ch != '/' {
1188 prev_token_was_hour = false;
1189 }
1190 }
1191 }
1192
1193 result
1194}
1195
1196fn count_repeats(
1197 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
1198 lower: char,
1199 upper: char,
1200) -> usize {
1201 let mut count = 0;
1202 while let Some(&c) = chars.peek() {
1203 if c == lower || c == upper {
1204 chars.next();
1205 count += 1;
1206 } else {
1207 break;
1208 }
1209 }
1210 count
1211}
1212
1213struct LocaleNames {
1214 short_months: [&'static str; 12],
1215 long_months: [&'static str; 12],
1216 short_weekdays: [&'static str; 7],
1217 long_weekdays: [&'static str; 7],
1218}
1219
1220fn locale_names(locale_code: Option<&str>) -> &'static LocaleNames {
1221 const ENGLISH: LocaleNames = LocaleNames {
1222 short_months: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
1223 long_months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
1224 short_weekdays: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
1225 long_weekdays: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
1226 };
1227 const ZH_CN: LocaleNames = LocaleNames {
1228 short_months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
1229 long_months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
1230 short_weekdays: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
1231 long_weekdays: ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"],
1232 };
1233 const ZH_TW: LocaleNames = LocaleNames {
1234 short_months: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
1235 long_months: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
1236 short_weekdays: ["週一", "週二", "週三", "週四", "週五", "週六", "週日"],
1237 long_weekdays: ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"],
1238 };
1239 const JA_JP: LocaleNames = LocaleNames {
1240 short_months: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
1241 long_months: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
1242 short_weekdays: ["月", "火", "水", "木", "金", "土", "日"],
1243 long_weekdays: ["月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日", "日曜日"],
1244 };
1245 const KO_KR: LocaleNames = LocaleNames {
1246 short_months: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
1247 long_months: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
1248 short_weekdays: ["월", "화", "수", "목", "금", "토", "일"],
1249 long_weekdays: ["월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"],
1250 };
1251 const DE_DE: LocaleNames = LocaleNames {
1252 short_months: ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"],
1253 long_months: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"],
1254 short_weekdays: ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"],
1255 long_weekdays: ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"],
1256 };
1257 const FR_FR: LocaleNames = LocaleNames {
1258 short_months: ["janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."],
1259 long_months: ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"],
1260 short_weekdays: ["lun.", "mar.", "mer.", "jeu.", "ven.", "sam.", "dim."],
1261 long_weekdays: ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"],
1262 };
1263 const ES_ES: LocaleNames = LocaleNames {
1264 short_months: ["ene", "feb", "mar", "abr", "mayo", "jun", "jul", "ago", "sept", "oct", "nov", "dic"],
1265 long_months: ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"],
1266 short_weekdays: ["lun", "mar", "mié", "jue", "vie", "sáb", "dom"],
1267 long_weekdays: ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"],
1268 };
1269 const IT_IT: LocaleNames = LocaleNames {
1270 short_months: ["gen", "feb", "mar", "apr", "mag", "giu", "lug", "ago", "set", "ott", "nov", "dic"],
1271 long_months: ["gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"],
1272 short_weekdays: ["lun", "mar", "mer", "gio", "ven", "sab", "dom"],
1273 long_weekdays: ["lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato", "domenica"],
1274 };
1275 const RU_RU: LocaleNames = LocaleNames {
1276 short_months: ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"],
1277 long_months: ["январь", "февраль", "март", "апрель", "май", "июнь", "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"],
1278 short_weekdays: ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"],
1279 long_weekdays: ["понедельник", "вторник", "среда", "четверг", "пятница", "суббота", "воскресенье"],
1280 };
1281
1282 let code = locale_code.unwrap_or("").to_ascii_uppercase();
1283 match code.as_str() {
1285 "804" | "1004" | "20804" => &ZH_CN,
1286 "404" | "1404" | "0C04" | "100C" | "140C" | "0404" => &ZH_TW,
1287 "411" | "10411" => &JA_JP,
1288 "412" | "10412" => &KO_KR,
1289 "407" => &DE_DE,
1290 "40C" => &FR_FR,
1291 "40A" => &ES_ES,
1292 "410" => &IT_IT,
1293 "419" => &RU_RU,
1294 _ => &ENGLISH,
1295 }
1296}
1297
1298fn short_month_name(month: u32, locale_code: Option<&str>) -> &'static str {
1299 if month < 1 || month > 12 {
1300 return "";
1301 }
1302 locale_names(locale_code).short_months[(month - 1) as usize]
1303}
1304
1305fn long_month_name(month: u32, locale_code: Option<&str>) -> &'static str {
1306 if month < 1 || month > 12 {
1307 return "";
1308 }
1309 locale_names(locale_code).long_months[(month - 1) as usize]
1310}
1311
1312fn short_weekday_name(wd: chrono::Weekday, locale_code: Option<&str>) -> &'static str {
1313 let idx = wd.num_days_from_monday() as usize;
1314 locale_names(locale_code).short_weekdays[idx]
1315}
1316
1317fn long_weekday_name(wd: chrono::Weekday, locale_code: Option<&str>) -> &'static str {
1318 let idx = wd.num_days_from_monday() as usize;
1319 locale_names(locale_code).long_weekdays[idx]
1320}
1321
1322#[cfg(test)]
1323mod tests {
1324 use super::*;
1325
1326 #[test]
1327 fn test_built_in_map() {
1328 assert_eq!(built_in_num_fmt_code(14), Some("mm-dd-yy"));
1329 assert_eq!(built_in_num_fmt_code(0), Some("General"));
1330 assert_eq!(built_in_num_fmt_code(9999), None);
1331 }
1332
1333 #[test]
1334 fn test_general() {
1335 assert_eq!(apply_number_format(1234.56, 0, None, false), "1234.56");
1336 assert_eq!(apply_number_format(100.0, 0, None, false), "100");
1337 }
1338
1339 #[test]
1340 fn test_number() {
1341 assert_eq!(apply_number_format(1234.5, 1, None, false), "1235");
1342 assert_eq!(apply_number_format(1234.5, 2, None, false), "1234.50");
1343 assert_eq!(apply_number_format(1234.5, 3, None, false), "1,235");
1344 assert_eq!(apply_number_format(1234.5, 4, None, false), "1,234.50");
1345 }
1346
1347 #[test]
1348 fn test_percentage() {
1349 assert_eq!(apply_number_format(0.1234, 9, None, false), "12%");
1350 assert_eq!(apply_number_format(0.1234, 10, None, false), "12.34%");
1351 }
1352
1353 #[test]
1354 fn test_scientific() {
1355 assert_eq!(apply_number_format(12345.0, 11, None, false), "1.23E+04");
1356 assert_eq!(apply_number_format(12345.0, 48, None, false), "1.2E+04");
1359 }
1360
1361 #[test]
1362 fn test_date() {
1363 let serial = 45486.0; assert_eq!(apply_number_format(serial, 14, None, false), "07-13-24");
1365 assert_eq!(apply_number_format(serial, 15, None, false), "13-Jul-24");
1366 assert_eq!(apply_number_format(serial, 16, None, false), "13-Jul");
1367 assert_eq!(apply_number_format(serial, 17, None, false), "Jul-24");
1368 }
1369
1370 #[test]
1371 fn test_time() {
1372 let serial = 0.5; assert_eq!(apply_number_format(serial, 18, None, false), "12:00 PM");
1374 assert_eq!(apply_number_format(serial, 20, None, false), "12:00");
1375 assert_eq!(apply_number_format(serial, 21, None, false), "12:00:00");
1376 }
1377
1378 #[test]
1379 fn test_custom_currency() {
1380 assert_eq!(
1381 apply_number_format(1234.5, 164, Some("$#,##0.00"), false),
1382 "$1,234.50"
1383 );
1384 }
1385
1386 #[test]
1387 fn test_custom_date() {
1388 let serial = 45486.0;
1389 assert_eq!(
1390 apply_number_format(serial, 164, Some("yyyy-mm-dd"), false),
1391 "2024-07-13"
1392 );
1393 }
1394
1395 #[test]
1396 fn test_is_date_time_pattern() {
1397 assert!(is_date_time_pattern(""));
1398 assert!(is_date_time_pattern("yyyy-mm-dd"));
1399 assert!(is_date_time_pattern("hh:mm:ss"));
1400 assert!(is_date_time_pattern("[h]:mm:ss"));
1401 assert!(is_date_time_pattern("m/d/yyyy h:mm AM/PM"));
1402 assert!(is_date_time_pattern("[$-409]dddd, mmmm d, yyyy"));
1403 assert!(is_date_time_pattern("[red]yyyy-mm-dd"));
1404 assert!(is_date_time_pattern("\\yyyy-mm-dd"));
1405 assert!(is_date_time_pattern("\"Date: \"yyyy-mm-dd"));
1406
1407 assert!(!is_date_time_pattern("0.00"));
1408 assert!(!is_date_time_pattern("#,##0"));
1409 assert!(!is_date_time_pattern("0%"));
1410 assert!(!is_date_time_pattern("0.00E+00"));
1411 assert!(!is_date_time_pattern("# ?/?"));
1412 assert!(!is_date_time_pattern("yyyy-mm-dd;0.00"));
1413 assert!(!is_date_time_pattern("@"));
1414 assert!(!is_date_time_pattern("yyyy-mm-dd;@"));
1415 assert!(!is_date_time_pattern("h:mm;@"));
1416 assert!(is_date_time_pattern("\"@\"yyyy-mm-dd"));
1417 }
1418
1419 #[test]
1420 fn test_builtin_accounting_formats() {
1421 assert_eq!(apply_number_format(1234.0, 37, None, false), "1,234 ");
1424 assert_eq!(apply_number_format(-1234.0, 37, None, false), "(1,234)");
1425 assert_eq!(apply_number_format(1234.0, 38, None, false), "1,234 ");
1426 assert_eq!(apply_number_format(-1234.0, 38, None, false), "(1,234)");
1427 assert_eq!(apply_number_format(1234.5, 39, None, false), "1,234.50 ");
1428 assert_eq!(apply_number_format(-1234.5, 39, None, false), "(1,234.50)");
1429 assert_eq!(apply_number_format(1234.5, 40, None, false), "1,234.50 ");
1430 assert_eq!(apply_number_format(-1234.5, 40, None, false), "(1,234.50)");
1431 }
1432
1433 #[test]
1434 fn test_elapsed_time() {
1435 assert_eq!(format_number(1.5, "[h]", false), "36");
1436 assert_eq!(format_number(1.5, "[m]", false), "2160");
1437 assert_eq!(format_number(1.5, "[s]", false), "129600");
1438 assert_eq!(format_number(1.5, "[h]:mm:ss", false), "36:00:00");
1439 }
1440
1441 #[test]
1442 fn test_locale_date_names() {
1443 let serial = 45486.0; assert_eq!(
1445 format_number(serial, "[$-409]dddd, mmmm d, yyyy", false),
1446 "Saturday, July 13, 2024"
1447 );
1448 assert_eq!(
1449 format_number(serial, "[$-804]dddd, mmmm d, yyyy", false),
1450 "星期六, 七月 13, 2024"
1451 );
1452 assert_eq!(
1453 format_number(serial, "[$-411]dddd, mmmm d, yyyy", false),
1454 "土曜日, 7月 13, 2024"
1455 );
1456 assert_eq!(
1457 format_number(serial, "[$-412]dddd, mmmm d, yyyy", false),
1458 "토요일, 7월 13, 2024"
1459 );
1460 }
1461
1462 #[test]
1463 fn test_dbnum_switch() {
1464 assert_eq!(format_number(1234.0, "[DBNum1]0", false), "一二三四");
1465 assert_eq!(format_number(1234.0, "[DBNum2]0", false), "壹贰叁肆");
1466 assert_eq!(format_number(1234.0, "[DBNum3]0", false), "1234");
1467 }
1468
1469 #[test]
1470 fn test_currency_locale_brackets() {
1471 assert_eq!(
1472 format_number(1234.5, "[$$]#,##0.00", false),
1473 "$1,234.50"
1474 );
1475 assert_eq!(
1476 format_number(1234.5, "[$€-40C]#,##0.00", false),
1477 "€1,234.50"
1478 );
1479 assert_eq!(
1480 format_number(45486.0, "[$-409]yyyy-mm-dd", false),
1481 "2024-07-13"
1482 );
1483 }
1484}