work
authorBen Pfaff <blp@cs.stanford.edu>
Sat, 3 Jan 2026 23:54:06 +0000 (15:54 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Sat, 3 Jan 2026 23:54:06 +0000 (15:54 -0800)
rust/pspp/src/format.rs
rust/pspp/src/format/decimals.rs
rust/pspp/src/spv/read/legacy_xml.rs

index 4dca95e1e6a29e331e63cefa83eaa50faa4d5bd6..c45da56aae2d795d17d42a92df123ed6217f550f 100644 (file)
@@ -90,19 +90,34 @@ pub enum Error {
     },
 }
 
+/// Format [Type] categories.
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub enum Category {
-    // Numeric formats.
+    /// F, COMMA, DOT, DOLLAR, PCT, and E.
     Basic,
+
+    /// CCx.
     Custom,
+
+    /// N and Z.
     Legacy,
+
+    /// P, PK, IB, PIB, and RB.
     Binary,
+
+    /// PIBHEX and RBHEX.
     Hex,
+
+    /// DATE, ADATE, EDATE, JDATE, SDATE, QYR, MOYR, WKYR, DATETIME, and YMDHMS.
     Date,
+
+    /// MTIME, TIME, and DTIME.
     Time,
+
+    /// WKDAY and MONTH.
     DateComponent,
 
-    // String formats.
+    // A and AHEX.
     String,
 }
 
index 12c7e80d9d35298c70c2e6cd9237448d8c23f86e..8813c00ff1060d2193c594a42830c351be91771a 100644 (file)
@@ -9,8 +9,8 @@
 //! <https://cldr.unicode.org/index/downloads>, rename it as
 //! `cldr-json-full.zip` in the same directory as `build.rs`,
 //! and touch `build.rs` to force a rebuild.
-use std::{collections::HashMap, sync::LazyLock};
 use crate::format::Decimal;
+use std::{collections::HashMap, sync::LazyLock};
 
 /// Map from language to decimal point.
 pub static LANG_TO_DECIMAL: LazyLock<HashMap<&'static str, Decimal>> = LazyLock::new(|| {
index e11dba677214587224f992472cf61f1b586e600c..4150ab8e21a4e7ccb4ee3820d931e8772d2c27a2 100644 (file)
@@ -213,21 +213,14 @@ impl Visualization {
             }
         }
         for label in footnote_labels {
-            for (index, text) in label.text().iter().enumerate() {
-                if let Some(uses_reference) = text.uses_reference {
-                    let entry = footnote_builder
-                        .entry(uses_reference.get() - 1)
-                        .or_default();
-                    if index % 2 == 1 {
-                        entry.content = text.text.strip_suffix('\n').unwrap_or(&text.text).into();
+            for (i, text) in label.text().iter().enumerate() {
+                if let DecodedText::FootnoteDefinition { index, text } = text.decode() {
+                    let entry = footnote_builder.entry(index).or_default();
+                    if i % 2 == 1 {
+                        entry.content = text.strip_suffix('\n').unwrap_or(text).into();
                     } else {
-                        entry.marker = Some(
-                            text.text
-                                .trim_end()
-                                .strip_suffix('.')
-                                .unwrap_or(&text.text)
-                                .into(),
-                        );
+                        entry.marker =
+                            Some(text.trim_end().strip_suffix('.').unwrap_or(text).into());
                     }
                 }
             }
@@ -2058,9 +2051,12 @@ enum LabelChild {
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 struct Text {
+    /// If this is present, then `text` defines the content or the marker for a
+    /// footnote.
     #[serde(rename = "@usesReference")]
     uses_reference: Option<NonZeroUsize>,
 
+    /// If this is present, then
     #[serde(rename = "@definesReference")]
     defines_reference: Option<NonZeroUsize>,
 
@@ -2068,6 +2064,34 @@ struct Text {
     text: String,
 }
 
+enum DecodedText<'a> {
+    /// Defines footnote `index` content or marker as `text`.
+    FootnoteDefinition { index: usize, text: &'a str },
+
+    /// Adds a reference to footnote `index`.
+    FootnoteReference { index: usize },
+
+    /// Text content.
+    Text { text: &'a str },
+}
+
+impl Text {
+    fn decode(&self) -> DecodedText<'_> {
+        if let Some(uses_reference) = self.uses_reference {
+            DecodedText::FootnoteDefinition {
+                index: uses_reference.get() - 1,
+                text: &self.text,
+            }
+        } else if let Some(defines_reference) = self.defines_reference {
+            DecodedText::FootnoteReference {
+                index: defines_reference.get() - 1,
+            }
+        } else {
+            DecodedText::Text { text: &self.text }
+        }
+    }
+}
+
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 struct LabelFrame {
@@ -2079,26 +2103,20 @@ impl LabelFrame {
         if !labels.is_empty() {
             let mut s = String::new();
             let mut f = Vec::new();
-            for t in labels {
-                if let LabelChild::Text(text) = &t.child {
-                    for t in text {
-                        if t.uses_reference.is_none() {
-                            if let Some(defines_reference) = t.defines_reference
-                                && let Some(footnote) = footnotes.get(defines_reference.get() - 1)
-                            {
-                                f.push(footnote);
-                            } else {
-                                s += &t.text;
+            for label in labels {
+                for text in label.text() {
+                    match text.decode() {
+                        DecodedText::FootnoteReference { index } => {
+                            if let Some(footnote) = footnotes.get(index) {
+                                f.push(footnote)
                             }
                         }
+                        DecodedText::Text { text } => s += text,
+                        _ => (),
                     }
                 }
             }
-            let mut value = Value::new_user_text(s);
-            for footnote in f {
-                value = value.with_footnote(footnote);
-            }
-            Some(value)
+            Some(Value::new_user_text(s).with_footnotes(f))
         } else {
             None
         }