1use std::borrow::Cow;
22use std::fmt::Write as _;
23
24use polars::prelude::{AnyValue, DataType};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum Grouping {
29 #[default]
31 None,
32 Thousands,
34 Indian,
36}
37
38impl Grouping {
39 fn separator_count(self, digits: usize) -> usize {
41 match self {
42 Grouping::None => 0,
43 Grouping::Thousands => digits.saturating_sub(1) / 3,
44 Grouping::Indian => {
45 if digits <= 3 {
46 0
47 } else {
48 1 + (digits - 4) / 2
49 }
50 }
51 }
52 }
53
54 #[inline]
57 fn breaks_after(self, digits_emitted: u32) -> bool {
58 match self {
59 Grouping::None => false,
60 Grouping::Thousands => digits_emitted.is_multiple_of(3),
61 Grouping::Indian => {
62 digits_emitted == 3 || (digits_emitted > 3 && digits_emitted % 2 == 1)
63 }
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct NumberFormat {
71 pub grouping: Grouping,
72 pub group_sep: char,
74 pub decimal_sep: char,
76 pub floats: bool,
78 pub float_precision: Option<u8>,
80}
81
82impl Default for NumberFormat {
83 fn default() -> Self {
84 Self::PLAIN
85 }
86}
87
88impl NumberFormat {
89 pub const PLAIN: Self = Self {
92 grouping: Grouping::None,
93 group_sep: ',',
94 decimal_sep: '.',
95 floats: true,
96 float_precision: None,
97 };
98
99 pub fn preset(name: &str) -> Option<Self> {
104 let base = Self::PLAIN;
105 Some(match name {
106 "none" | "plain" => base,
108 "thousands" => Self {
110 grouping: Grouping::Thousands,
111 group_sep: ',',
112 decimal_sep: '.',
113 ..base
114 },
115 "european" => Self {
117 grouping: Grouping::Thousands,
118 group_sep: '.',
119 decimal_sep: ',',
120 ..base
121 },
122 "si" => Self {
124 grouping: Grouping::Thousands,
125 group_sep: '\u{202f}', decimal_sep: '.',
127 ..base
128 },
129 "swiss" => Self {
131 grouping: Grouping::Thousands,
132 group_sep: '\'',
133 decimal_sep: '.',
134 ..base
135 },
136 "indian" => Self {
138 grouping: Grouping::Indian,
139 group_sep: ',',
140 decimal_sep: '.',
141 ..base
142 },
143 "underscore" => Self {
145 grouping: Grouping::Thousands,
146 group_sep: '_',
147 decimal_sep: '.',
148 ..base
149 },
150 _ => return None,
151 })
152 }
153
154 pub const CHROME: Self = Self {
157 grouping: Grouping::Thousands,
158 group_sep: ',',
159 decimal_sep: '.',
160 floats: true,
161 float_precision: None,
162 };
163
164 pub const PRESET_NAMES: &'static [&'static str] = &[
166 "none",
167 "thousands",
168 "european",
169 "si",
170 "swiss",
171 "indian",
172 "underscore",
173 ];
174
175 pub fn is_noop(&self) -> bool {
178 self.grouping == Grouping::None && self.decimal_sep == '.' && self.float_precision.is_none()
179 }
180
181 #[inline]
188 fn groups(&self) -> bool {
189 self.grouping != Grouping::None
190 }
191
192 pub fn width_i64(&self, v: i64) -> usize {
195 self.width_u64(v.unsigned_abs()) + usize::from(v < 0)
196 }
197
198 pub fn width_u64(&self, v: u64) -> usize {
200 let digits = digit_count(v);
201 let seps = if self.groups() {
202 self.grouping.separator_count(digits)
203 } else {
204 0
205 };
206 digits + seps
207 }
208
209 pub fn write_i64(&self, v: i64, out: &mut String) -> usize {
211 self.write_magnitude(v.unsigned_abs(), v < 0, out)
212 }
213
214 pub fn write_u64(&self, v: u64, out: &mut String) -> usize {
216 self.write_magnitude(v, false, out)
217 }
218
219 fn write_magnitude(&self, mag: u64, negative: bool, out: &mut String) -> usize {
222 let mut buf = [0u8; 64];
225 let mut pos = buf.len();
226 let mut width = 0usize;
227
228 let group = self.groups();
229 let mut sep_bytes = [0u8; 4];
230 let sep = self.group_sep.encode_utf8(&mut sep_bytes);
231 let sep = sep.as_bytes();
232
233 let mut n = mag;
234 let mut emitted: u32 = 0;
235 loop {
236 let d = (n % 10) as u8;
237 n /= 10;
238 pos -= 1;
239 buf[pos] = b'0' + d;
240 emitted += 1;
241 width += 1;
242 if n == 0 {
243 break;
244 }
245 if group && self.grouping.breaks_after(emitted) {
246 pos -= sep.len();
247 buf[pos..pos + sep.len()].copy_from_slice(sep);
248 width += 1;
249 }
250 }
251
252 if negative {
253 pos -= 1;
254 buf[pos] = b'-';
255 width += 1;
256 }
257
258 debug_assert!(std::str::from_utf8(&buf[pos..]).is_ok());
261 match std::str::from_utf8(&buf[pos..]) {
262 Ok(s) => {
263 out.push_str(s);
264 width
265 }
266 Err(_) => 0,
271 }
272 }
273
274 pub fn write_f64(&self, v: f64, scratch: &mut String, out: &mut String) -> usize {
279 scratch.clear();
280 match self.float_precision {
281 Some(p) => {
282 let _ = write!(scratch, "{:.*}", p as usize, v);
283 }
284 None => {
285 let _ = write!(scratch, "{}", v);
286 }
287 }
288 self.regroup_decimal(scratch, out)
289 }
290
291 pub fn regroup_decimal(&self, src: &str, out: &mut String) -> usize {
299 let body = src.strip_prefix('-').unwrap_or(src);
300 let negative = body.len() != src.len();
301 let (int_part, frac_part) = match body.find('.') {
302 Some(i) => (&body[..i], Some(&body[i + 1..])),
303 None => (body, None),
304 };
305
306 let plain = !int_part.is_empty()
307 && int_part.bytes().all(|b| b.is_ascii_digit())
308 && frac_part.is_none_or(|f| f.bytes().all(|b| b.is_ascii_digit()));
309 if !plain {
310 out.push_str(src);
312 return src.chars().count();
313 }
314
315 let mut width = 0usize;
316 if negative {
317 out.push('-');
318 width += 1;
319 }
320
321 let digits = int_part.len();
322 if self.groups() {
323 for (i, ch) in int_part.chars().enumerate() {
324 let remaining = (digits - i) as u32;
327 if i > 0 && self.grouping.breaks_after(remaining) {
328 out.push(self.group_sep);
329 width += 1;
330 }
331 out.push(ch);
332 width += 1;
333 }
334 } else {
335 out.push_str(int_part);
336 width += digits;
337 }
338
339 if let Some(frac) = frac_part {
340 out.push(self.decimal_sep);
341 width += 1 + frac.len();
342 out.push_str(frac);
343 }
344 width
345 }
346}
347
348pub fn group_chrome(n: usize) -> String {
356 let mut out = String::new();
357 NumberFormat::CHROME.write_u64(n as u64, &mut out);
358 out
359}
360
361#[inline]
363fn digit_count(n: u64) -> usize {
364 n.checked_ilog10().map_or(0, |l| l as usize) + 1
365}
366
367#[derive(Debug, Clone, PartialEq)]
369pub enum CellFormatter {
370 Passthrough,
372 Number(NumberFormat),
374}
375
376impl CellFormatter {
377 #[inline]
378 pub fn is_passthrough(&self) -> bool {
379 matches!(self, CellFormatter::Passthrough)
380 }
381}
382
383pub fn is_numeric_dtype(dtype: &DataType) -> bool {
385 matches!(
386 dtype,
387 DataType::Int8
388 | DataType::Int16
389 | DataType::Int32
390 | DataType::Int64
391 | DataType::UInt8
392 | DataType::UInt16
393 | DataType::UInt32
394 | DataType::UInt64
395 | DataType::Float32
396 | DataType::Float64
397 )
398}
399
400pub fn is_right_aligned_dtype(dtype: &DataType) -> bool {
406 is_numeric_dtype(dtype)
407}
408
409#[derive(Debug, Clone)]
411pub struct NumberFormatSettings {
412 pub format: NumberFormat,
414 pub enabled: bool,
416 pub exclude: Vec<Glob>,
418 pub align_numeric_right: bool,
420}
421
422impl Default for NumberFormatSettings {
423 fn default() -> Self {
424 Self {
425 format: NumberFormat::PLAIN,
426 enabled: true,
427 exclude: Vec::new(),
428 align_numeric_right: true,
429 }
430 }
431}
432
433impl NumberFormatSettings {
434 pub fn formatter_for(&self, col_name: &str, dtype: &DataType) -> CellFormatter {
437 if !self.enabled || self.format.is_noop() || !is_numeric_dtype(dtype) {
438 return CellFormatter::Passthrough;
439 }
440 if self.exclude.iter().any(|g| g.matches(col_name)) {
441 return CellFormatter::Passthrough;
442 }
443 let mut fmt = self.format.clone();
444 if !fmt.floats && matches!(dtype, DataType::Float32 | DataType::Float64) {
445 fmt.grouping = Grouping::None;
448 if fmt.is_noop() {
449 return CellFormatter::Passthrough;
450 }
451 }
452 CellFormatter::Number(fmt)
453 }
454}
455
456pub fn format_any_value<'v>(
461 fmt: &CellFormatter,
462 value: &'v AnyValue<'v>,
463 scratch: &mut String,
464) -> Cow<'v, str> {
465 if matches!(value, AnyValue::Null) {
466 return Cow::Borrowed("");
467 }
468 let nf = match fmt {
469 CellFormatter::Passthrough => return crate::exact::str_value(value),
470 CellFormatter::Number(nf) => nf,
471 };
472 let mut out = String::new();
473 match *value {
474 AnyValue::Int8(v) => nf.write_i64(v as i64, &mut out),
475 AnyValue::Int16(v) => nf.write_i64(v as i64, &mut out),
476 AnyValue::Int32(v) => nf.write_i64(v as i64, &mut out),
477 AnyValue::Int64(v) => nf.write_i64(v, &mut out),
478 AnyValue::UInt8(v) => nf.write_u64(v as u64, &mut out),
479 AnyValue::UInt16(v) => nf.write_u64(v as u64, &mut out),
480 AnyValue::UInt32(v) => nf.write_u64(v as u64, &mut out),
481 AnyValue::UInt64(v) => nf.write_u64(v, &mut out),
482 AnyValue::Float32(f) => match nf.float_precision {
486 Some(_) => nf.write_f64(f as f64, scratch, &mut out),
487 None => nf.regroup_decimal(&value.str_value(), &mut out),
488 },
489 AnyValue::Float64(f) => match nf.float_precision {
490 Some(_) => nf.write_f64(f, scratch, &mut out),
491 None => nf.regroup_decimal(&value.str_value(), &mut out),
492 },
493 _ => return crate::exact::str_value(value),
494 };
495 Cow::Owned(out)
496}
497
498pub fn display_width(fmt: &CellFormatter, value: &AnyValue, scratch: &mut String) -> usize {
503 if matches!(value, AnyValue::Null) {
504 return 0;
505 }
506 if let CellFormatter::Number(nf) = fmt {
507 match *value {
508 AnyValue::Int8(v) => return nf.width_i64(v as i64),
509 AnyValue::Int16(v) => return nf.width_i64(v as i64),
510 AnyValue::Int32(v) => return nf.width_i64(v as i64),
511 AnyValue::Int64(v) => return nf.width_i64(v),
512 AnyValue::UInt8(v) => return nf.width_u64(v as u64),
513 AnyValue::UInt16(v) => return nf.width_u64(v as u64),
514 AnyValue::UInt32(v) => return nf.width_u64(v as u64),
515 AnyValue::UInt64(v) => return nf.width_u64(v),
516 _ => {}
517 }
518 }
519 crate::glyphs::cell_width(&format_any_value(fmt, value, scratch))
520}
521
522#[derive(Debug, Clone, PartialEq, Eq)]
527pub struct Glob {
528 pattern: String,
529 has_wildcard: bool,
530}
531
532impl Glob {
533 pub fn new(pattern: impl Into<String>) -> Self {
534 let pattern = pattern.into();
535 let has_wildcard = pattern.contains('*') || pattern.contains('?');
536 Self {
537 pattern,
538 has_wildcard,
539 }
540 }
541
542 pub fn matches(&self, name: &str) -> bool {
543 if !self.has_wildcard {
544 return self.pattern == name;
545 }
546 let p: Vec<char> = self.pattern.chars().collect();
547 let n: Vec<char> = name.chars().collect();
548 let (mut pi, mut ni) = (0usize, 0usize);
550 let (mut star, mut mark) = (usize::MAX, 0usize);
551 while ni < n.len() {
552 if pi < p.len() && p[pi] == '*' {
557 star = pi;
558 mark = ni;
559 pi += 1;
560 } else if pi < p.len() && (p[pi] == '?' || p[pi] == n[ni]) {
561 pi += 1;
562 ni += 1;
563 } else if star != usize::MAX {
564 pi = star + 1;
565 mark += 1;
566 ni = mark;
567 } else {
568 return false;
569 }
570 }
571 while pi < p.len() && p[pi] == '*' {
572 pi += 1;
573 }
574 pi == p.len()
575 }
576}
577
578pub fn preset_for_locale_tag(tag: &str) -> &'static str {
585 let base = tag
587 .split(['.', '@'])
588 .next()
589 .unwrap_or(tag)
590 .replace('_', "-");
591 let lower = base.to_ascii_lowercase();
592 let lang = lower.split('-').next().unwrap_or(&lower);
593 let region = lower.split('-').nth(1).unwrap_or("");
594
595 if region == "ch" {
597 return "swiss";
598 }
599 match lang {
600 "de" | "es" | "it" | "pt" | "nl" | "id" | "tr" | "da" | "el" | "ro" | "ca" | "vi"
601 | "sl" | "hr" | "sr" | "is" => "european",
602 "fr" | "nb" | "no" | "sv" | "fi" | "cs" | "sk" | "pl" | "ru" | "uk" | "hu" | "lv"
603 | "lt" | "et" | "bg" => "si",
604 "hi" | "bn" | "ta" | "te" | "mr" | "gu" | "kn" | "ml" | "pa" | "or" | "as" | "ne" => {
605 "indian"
606 }
607 _ => "thousands",
609 }
610}
611
612pub fn system_locale_tag() -> Option<String> {
615 for var in ["LC_ALL", "LC_NUMERIC", "LANG"] {
616 if let Ok(v) = std::env::var(var) {
617 let v = v.trim();
618 if v.is_empty() {
619 continue;
620 }
621 if v == "C" || v == "POSIX" || v.starts_with("C.") {
622 return None;
623 }
624 return Some(v.to_string());
625 }
626 }
627 None
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633
634 fn fmt_i64(nf: &NumberFormat, v: i64) -> String {
635 let mut s = String::new();
636 let w = nf.write_i64(v, &mut s);
637 assert_eq!(w, s.chars().count(), "reported width disagrees with output");
638 assert_eq!(w, nf.width_i64(v), "width_i64 disagrees with write_i64");
639 s
640 }
641
642 fn thousands() -> NumberFormat {
643 NumberFormat::preset("thousands").unwrap()
644 }
645
646 #[test]
649 fn a_date_past_the_calendar_is_its_stored_number_in_a_cell() {
650 let mut scratch = String::new();
651 let thousands = NumberFormatSettings {
652 format: NumberFormat::preset("thousands").unwrap(),
653 ..NumberFormatSettings::default()
654 };
655 let value = AnyValue::Datetime(i64::MIN + 1, polars::prelude::TimeUnit::Microseconds, None);
656 let dtype = DataType::Datetime(polars::prelude::TimeUnit::Microseconds, None);
657 for fmt in [
658 CellFormatter::Passthrough,
659 thousands.formatter_for("t", &dtype),
660 CellFormatter::Number(NumberFormat::preset("thousands").unwrap()),
661 ] {
662 let text = format_any_value(&fmt, &value, &mut scratch).into_owned();
663 assert_eq!(text, "-9223372036854775807 us since 1970-01-01 UTC");
664 assert_eq!(display_width(&fmt, &value, &mut scratch), text.len());
665 }
666 let date = AnyValue::Date(i32::MAX);
667 assert_eq!(
668 format_any_value(&CellFormatter::Passthrough, &date, &mut scratch),
669 "2147483647 days since 1970-01-01"
670 );
671 }
672 #[test]
673 fn digit_count_basics() {
674 assert_eq!(digit_count(0), 1);
675 assert_eq!(digit_count(9), 1);
676 assert_eq!(digit_count(10), 2);
677 assert_eq!(digit_count(999), 3);
678 assert_eq!(digit_count(1000), 4);
679 assert_eq!(digit_count(u64::MAX), 20);
680 }
681
682 #[test]
683 fn every_value_in_a_formatted_column_is_grouped() {
684 let nf = thousands();
688 assert_eq!(fmt_i64(&nf, 0), "0");
689 assert_eq!(fmt_i64(&nf, 999), "999");
690 assert_eq!(fmt_i64(&nf, 1000), "1,000");
691 assert_eq!(fmt_i64(&nf, 2024), "2,024");
692 assert_eq!(fmt_i64(&nf, 9999), "9,999");
693 assert_eq!(fmt_i64(&nf, 10000), "10,000");
694 assert_eq!(fmt_i64(&nf, 1234567), "1,234,567");
695 }
696
697 #[test]
698 fn negatives_and_extremes() {
699 let nf = thousands();
700 assert_eq!(fmt_i64(&nf, -1234567), "-1,234,567");
701 assert_eq!(fmt_i64(&nf, -999), "-999");
702 assert_eq!(fmt_i64(&nf, i64::MIN), "-9,223,372,036,854,775,808");
703 assert_eq!(fmt_i64(&nf, i64::MAX), "9,223,372,036,854,775,807");
704
705 let mut s = String::new();
706 let w = nf.write_u64(u64::MAX, &mut s);
707 assert_eq!(s, "18,446,744,073,709,551,615");
708 assert_eq!(w, s.chars().count());
709 assert_eq!(w, nf.width_u64(u64::MAX));
710 }
711
712 #[test]
713 fn bed_style_coordinates() {
714 let nf = thousands();
716 assert_eq!(fmt_i64(&nf, 248_956_422), "248,956,422");
717 assert_eq!(fmt_i64(&nf, 3_088_269_832), "3,088,269,832");
718 }
719
720 #[test]
721 fn indian_grouping() {
722 let nf = NumberFormat::preset("indian").unwrap();
723 assert_eq!(fmt_i64(&nf, 100), "100");
724 assert_eq!(fmt_i64(&nf, 1000), "1,000");
725 assert_eq!(fmt_i64(&nf, 12345), "12,345");
726 assert_eq!(fmt_i64(&nf, 123456), "1,23,456");
727 assert_eq!(fmt_i64(&nf, 1234567), "12,34,567");
728 assert_eq!(fmt_i64(&nf, 12345678), "1,23,45,678");
729 assert_eq!(fmt_i64(&nf, -12345678), "-1,23,45,678");
730 }
731
732 #[test]
733 fn all_presets_render() {
734 let cases = [
735 ("none", "1234567"),
736 ("thousands", "1,234,567"),
737 ("european", "1.234.567"),
738 ("si", "1\u{202f}234\u{202f}567"),
739 ("swiss", "1'234'567"),
740 ("indian", "12,34,567"),
741 ("underscore", "1_234_567"),
742 ];
743 for (name, expected) in cases {
744 let nf = NumberFormat::preset(name).unwrap();
745 assert_eq!(fmt_i64(&nf, 1234567), expected, "preset {name}");
746 }
747 assert!(NumberFormat::preset("klingon").is_none());
748 for name in NumberFormat::PRESET_NAMES {
749 assert!(NumberFormat::preset(name).is_some(), "preset {name}");
750 }
751 }
752
753 #[test]
754 fn width_matches_rendered_length_across_range() {
755 for nf in NumberFormat::PRESET_NAMES
756 .iter()
757 .map(|n| NumberFormat::preset(n).unwrap())
758 {
759 let mut v: i64 = 1;
760 for _ in 0..19 {
761 for probe in [v, v - 1, -v, v * 3 / 2] {
762 let mut s = String::new();
763 let w = nf.write_i64(probe, &mut s);
764 assert_eq!(w, s.chars().count(), "{:?} on {probe}", nf.grouping);
765 assert_eq!(w, nf.width_i64(probe), "{:?} on {probe}", nf.grouping);
766 }
767 v = v.saturating_mul(10);
768 }
769 }
770 }
771
772 #[test]
773 fn floats_regroup_integer_part_only() {
774 let nf = thousands();
775 let mut s = String::new();
776 let w = nf.regroup_decimal("1234567.891", &mut s);
777 assert_eq!(s, "1,234,567.891");
778 assert_eq!(w, s.chars().count());
779
780 s.clear();
781 nf.regroup_decimal("-1234.5", &mut s);
782 assert_eq!(s, "-1,234.5");
783 }
784
785 #[test]
786 fn european_swaps_decimal_separator() {
787 let nf = NumberFormat::preset("european").unwrap();
788 let mut s = String::new();
789 let w = nf.regroup_decimal("1234567.89", &mut s);
790 assert_eq!(s, "1.234.567,89");
791 assert_eq!(w, s.chars().count());
792 }
793
794 #[test]
795 fn non_decimal_strings_pass_through_untouched() {
796 let nf = thousands();
797 for src in ["NaN", "inf", "-inf", "1e300", "1.5e-8", ""] {
798 let mut s = String::new();
799 let w = nf.regroup_decimal(src, &mut s);
800 assert_eq!(s, src, "{src} should pass through");
801 assert_eq!(w, src.chars().count());
802 }
803 }
804
805 #[test]
806 fn float_precision_is_applied() {
807 let nf = NumberFormat {
808 float_precision: Some(2),
809 ..thousands()
810 };
811 let (mut scratch, mut out) = (String::new(), String::new());
812 let w = nf.write_f64(1234.5678, &mut scratch, &mut out);
813 assert_eq!(out, "1,234.57");
814 assert_eq!(w, out.chars().count());
815
816 out.clear();
817 nf.write_f64(-0.5, &mut scratch, &mut out);
818 assert_eq!(out, "-0.50");
819 }
820
821 #[test]
822 fn is_noop_detects_the_free_path() {
823 assert!(NumberFormat::PLAIN.is_noop());
824 assert!(!thousands().is_noop());
825 assert!(
826 !NumberFormat {
827 float_precision: Some(2),
828 ..NumberFormat::PLAIN
829 }
830 .is_noop()
831 );
832 assert!(
833 !NumberFormat {
834 decimal_sep: ',',
835 ..NumberFormat::PLAIN
836 }
837 .is_noop()
838 );
839 }
840
841 #[test]
842 fn chrome_grouping_is_unconditional() {
843 assert_eq!(group_chrome(0), "0");
846 assert_eq!(group_chrome(999), "999");
847 assert_eq!(group_chrome(1234), "1,234");
848 assert_eq!(group_chrome(1_234_567), "1,234,567");
849 assert_eq!(group_chrome(usize::MAX), "18,446,744,073,709,551,615");
850 }
851
852 #[test]
853 fn glob_matching() {
854 assert!(Glob::new("year").matches("year"));
855 assert!(!Glob::new("year").matches("years"));
856 assert!(Glob::new("*_id").matches("sample_id"));
857 assert!(Glob::new("*_id").matches("_id"));
858 assert!(!Glob::new("*_id").matches("id_sample"));
859 assert!(Glob::new("chrom*").matches("chromStart"));
860 assert!(Glob::new("*").matches("anything"));
861 assert!(Glob::new("c?rom").matches("chrom"));
862 assert!(!Glob::new("c?rom").matches("chhrom"));
863 assert!(Glob::new("a*b*c").matches("axxbyyc"));
864 assert!(!Glob::new("a*b*c").matches("axxbyy"));
865
866 assert!(Glob::new("*").matches("*]"));
871 assert!(Glob::new("*").matches("a*b"));
872 assert!(Glob::new("*").matches("?"));
873 assert!(Glob::new("a*c").matches("a*c"));
874 assert!(Glob::new("a*c").matches("a*x*c"));
875 assert!(Glob::new("?").matches("*"));
876 }
877
878 #[test]
879 fn settings_resolve_per_column() {
880 let settings = NumberFormatSettings {
881 format: thousands(),
882 enabled: true,
883 exclude: vec![Glob::new("*_id"), Glob::new("year")],
884 align_numeric_right: true,
885 };
886 assert!(
888 !settings
889 .formatter_for("chromStart", &DataType::Int64)
890 .is_passthrough()
891 );
892 assert!(
894 settings
895 .formatter_for("chrom", &DataType::String)
896 .is_passthrough()
897 );
898 assert!(
899 settings
900 .formatter_for("when", &DataType::Date)
901 .is_passthrough()
902 );
903 assert!(
905 settings
906 .formatter_for("sample_id", &DataType::Int64)
907 .is_passthrough()
908 );
909 assert!(
910 settings
911 .formatter_for("year", &DataType::Int32)
912 .is_passthrough()
913 );
914 }
915
916 #[test]
917 fn settings_disabled_is_all_passthrough() {
918 let settings = NumberFormatSettings {
919 format: thousands(),
920 enabled: false,
921 ..Default::default()
922 };
923 assert!(
924 settings
925 .formatter_for("chromStart", &DataType::Int64)
926 .is_passthrough()
927 );
928 }
929
930 #[test]
931 fn plain_format_is_always_passthrough() {
932 let settings = NumberFormatSettings::default();
933 assert!(
934 settings
935 .formatter_for("chromStart", &DataType::Int64)
936 .is_passthrough()
937 );
938 }
939
940 #[test]
941 fn floats_flag_disables_grouping_for_floats_only() {
942 let settings = NumberFormatSettings {
943 format: NumberFormat {
944 floats: false,
945 ..thousands()
946 },
947 ..Default::default()
948 };
949 assert!(
950 !settings
951 .formatter_for("count", &DataType::Int64)
952 .is_passthrough()
953 );
954 assert!(
955 settings
956 .formatter_for("ratio", &DataType::Float64)
957 .is_passthrough()
958 );
959 }
960
961 #[test]
962 fn any_value_formatting_and_width_agree() {
963 let fmt = CellFormatter::Number(thousands());
964 let mut scratch = String::new();
965 let cases: Vec<AnyValue> = vec![
966 AnyValue::Int32(1234567),
967 AnyValue::Int64(-9876543),
968 AnyValue::UInt32(4000000),
969 AnyValue::UInt64(u64::MAX),
970 AnyValue::Int8(-12),
971 ];
972 for v in cases {
973 let s = format_any_value(&fmt, &v, &mut scratch).into_owned();
974 assert_eq!(
975 display_width(&fmt, &v, &mut scratch),
976 s.chars().count(),
977 "width mismatch for {v:?} -> {s}"
978 );
979 }
980 assert_eq!(
981 format_any_value(&fmt, &AnyValue::Int32(1234567), &mut scratch),
982 "1,234,567"
983 );
984 }
985
986 #[test]
987 fn nulls_and_strings_are_untouched() {
988 let fmt = CellFormatter::Number(thousands());
989 let mut scratch = String::new();
990 assert_eq!(format_any_value(&fmt, &AnyValue::Null, &mut scratch), "");
991 assert_eq!(
993 format_any_value(&fmt, &AnyValue::String("chr1"), &mut scratch),
994 "chr1"
995 );
996 assert_eq!(
997 format_any_value(
998 &CellFormatter::Passthrough,
999 &AnyValue::Int64(1234567),
1000 &mut scratch
1001 ),
1002 "1234567"
1003 );
1004 }
1005
1006 #[test]
1009 fn number_format_values_match_presets() {
1010 let mut cli: Vec<&str> = datui_cli::NUMBER_FORMAT_VALUES.to_vec();
1011 let mut expected: Vec<&str> = NumberFormat::PRESET_NAMES.to_vec();
1012 expected.push("system");
1014 cli.sort_unstable();
1015 expected.sort_unstable();
1016 assert_eq!(
1017 cli, expected,
1018 "datui_cli::NUMBER_FORMAT_VALUES is out of sync with NumberFormat::PRESET_NAMES"
1019 );
1020 }
1021
1022 #[test]
1023 fn locale_tags_map_to_presets() {
1024 assert_eq!(preset_for_locale_tag("en_US.UTF-8"), "thousands");
1025 assert_eq!(preset_for_locale_tag("de_DE.UTF-8"), "european");
1026 assert_eq!(preset_for_locale_tag("de_DE.UTF-8@euro"), "european");
1027 assert_eq!(preset_for_locale_tag("fr_FR"), "si");
1028 assert_eq!(preset_for_locale_tag("hi_IN"), "indian");
1029 assert_eq!(preset_for_locale_tag("de_CH"), "swiss");
1030 assert_eq!(preset_for_locale_tag("it-CH"), "swiss");
1031 assert_eq!(preset_for_locale_tag("ja_JP"), "thousands");
1032 assert_eq!(preset_for_locale_tag("xx_YY"), "thousands");
1034 }
1035}