work on testing CCx
authorBen Pfaff <blp@cs.stanford.edu>
Sat, 22 Mar 2025 16:44:15 +0000 (09:44 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sat, 22 Mar 2025 16:44:15 +0000 (09:44 -0700)
rust/pspp/src/format/mod.rs

index a3cb174944e0b98fc7325b45e2e5492aebb2a4e6..9f85588eedca828ea7dde555b4c0379874511bcf 100644 (file)
@@ -4,7 +4,7 @@ use std::{
     fmt::{Display, Error as FmtError, Formatter, Result as FmtResult, Write},
     io::Write as _,
     ops::{Not, RangeInclusive},
-    str::{from_utf8_unchecked, FromStr},
+    str::{from_utf8_unchecked, Chars, FromStr},
     sync::LazyLock,
 };
 
@@ -777,6 +777,18 @@ impl From<Decimal> for u8 {
     }
 }
 
+impl TryFrom<char> for Decimal {
+    type Error = ();
+
+    fn try_from(c: char) -> Result<Self, Self::Error> {
+        match c {
+            '.' => Ok(Self::Dot),
+            ',' => Ok(Self::Comma),
+            _ => Err(()),
+        }
+    }
+}
+
 impl Not for Decimal {
     type Output = Self;
 
@@ -982,21 +994,100 @@ pub struct Affix {
 }
 
 impl Affix {
-    fn new(s: &str) -> Self {
+    fn new(s: impl Into<String>) -> Self {
+        let s = s.into();
         Self {
-            s: s.to_string(),
             width: s.width(),
+            s,
         }
     }
+
+    fn extra_bytes(&self) -> usize {
+        self.s.len().checked_sub(self.width).unwrap()
+    }
 }
 
-pub struct DisplayValue<'a> {
+impl FromStr for NumberStyle {
+    type Err = ();
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        fn find_separator(s: &str) -> Option<char> {
+            // Count commas and periods.  There must be exactly three of one or
+            // the other, except that an apostrophe escapes a following comma or
+            // period.
+            let mut n_commas = 0;
+            let mut n_periods = 0;
+            let s = s.as_bytes();
+            for i in 0..s.len() {
+                if i > 0 && s[i - 1] == b'\'' {
+                } else if s[i] == b',' {
+                    n_commas += 1;
+                } else if s[i] == b'.' {
+                    n_periods += 1;
+                }
+            }
+
+            if n_commas == 3 && n_periods != 3 {
+                Some(',')
+            } else if n_periods == 3 && n_commas != 3 {
+                Some('.')
+            } else {
+                None
+            }
+        }
+
+        fn take_cc_token(iter: &mut Chars<'_>, grouping: char) -> Affix {
+            let mut s = String::new();
+            let mut quote = false;
+            for c in iter {
+                if c == '\'' {
+                    quote = true;
+                } else {
+                    if c == grouping && !quote {
+                        break;
+                    }
+                    s.push(c);
+                    quote = false;
+                }
+            }
+            Affix::new(s)
+        }
+
+        let Some(grouping) = find_separator(s) else {
+            return Err(());
+        };
+        let mut iter = s.chars();
+        let neg_prefix = take_cc_token(&mut iter, grouping);
+        let prefix = take_cc_token(&mut iter, grouping);
+        let suffix = take_cc_token(&mut iter, grouping);
+        let neg_suffix = take_cc_token(&mut iter, grouping);
+        let grouping: Decimal = grouping.try_into().unwrap();
+        let decimal = !grouping;
+        let extra_bytes = neg_prefix.extra_bytes()
+            + prefix.extra_bytes()
+            + suffix.extra_bytes()
+            + neg_suffix.extra_bytes();
+        Ok(Self {
+            neg_prefix,
+            prefix,
+            suffix,
+            neg_suffix,
+            decimal,
+            grouping: Some(grouping),
+            leading_zero: false,
+            extra_bytes,
+        })
+    }
+}
+
+pub struct DisplayValue<'a, 'b> {
     format: Format,
+    settings: &'b Settings,
     value: &'a Value,
     encoding: &'static Encoding,
 }
 
-impl<'a> Display for DisplayValue<'a> {
+impl<'a, 'b> Display for DisplayValue<'a, 'b> {
     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
         let number = match self.value {
             Value::Number(number) => *number,
@@ -1055,19 +1146,21 @@ impl<'a> Display for DisplayValue<'a> {
     }
 }
 
-impl<'a> DisplayValue<'a> {
+impl<'a, 'b> DisplayValue<'a, 'b> {
     pub fn new(format: Format, value: &'a Value, encoding: &'static Encoding) -> Self {
         Self {
             format,
             value,
             encoding,
+            settings: &PsppSettings::global().formats,
         }
     }
+    pub fn with_settings(self, settings: &'b Settings) -> Self {
+        Self { settings, ..self }
+    }
     fn number(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
         if number.is_finite() {
-            let style = PsppSettings::global()
-                .formats
-                .number_style(self.format.type_);
+            let style = self.settings.number_style(self.format.type_);
             if self.format.type_ != Type::E && number.abs() < 1.5 * power10(self.format.w()) {
                 let rounder = Rounder::new(style, number, self.format.d);
                 if self.decimal(f, &rounder, style, true)?
@@ -1419,7 +1512,7 @@ impl<'a> DisplayValue<'a> {
                     }
                 }
                 b'y' => {
-                    let epoch = PsppSettings::global().formats.epoch.0;
+                    let epoch = self.settings.epoch.0;
                     let offset = date.year() - epoch;
                     if offset < 0 || offset > 99 {
                         return self.overflow(f);
@@ -1470,7 +1563,7 @@ impl<'a> DisplayValue<'a> {
                         let d = min(self.format.d(), excess_width as usize);
                         let w = d + 3;
                         write!(&mut output, ":{:02$.*}", d, number, w).unwrap();
-                        if PsppSettings::global().formats.decimal == Decimal::Comma {
+                        if self.settings.decimal == Decimal::Comma {
                             fix_decimal_point(&mut output);
                         }
                     }
@@ -1876,7 +1969,7 @@ mod test {
 
     use crate::{
         dictionary::Value,
-        format::{AbstractFormat, Format, UncheckedFormat},
+        format::{AbstractFormat, Format, Settings, UncheckedFormat},
         lex::{
             scan::StringScanner,
             segment::Syntax,
@@ -1889,6 +1982,7 @@ mod test {
             .join("src/format/testdata")
             .join(name);
         let input = BufReader::new(File::open(&filename).unwrap());
+        let settings = Settings::default();
         let mut value = 0.0;
         let mut value_name = String::new();
         for (line_number, line) in input.lines().map(|r| r.unwrap()).enumerate() {