work
authorBen Pfaff <blp@cs.stanford.edu>
Sat, 1 Nov 2025 00:01:05 +0000 (17:01 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sat, 1 Nov 2025 00:01:05 +0000 (17:01 -0700)
rust/pspp/src/calendar.rs
rust/pspp/src/output/drivers/spv.rs
rust/pspp/src/output/pivot.rs
rust/pspp/src/output/pivot/look_xml.rs
rust/pspp/src/output/spv.rs
rust/pspp/src/output/spv/legacy_bin.rs
rust/pspp/src/output/spv/legacy_xml.rs
rust/pspp/src/output/spv/light.rs

index 8da918863e572d7fb7c14b3a8569b857d90d64c4..808a0d3bc03781f7fdbf03ae73f224e6369ea364 100644 (file)
@@ -27,6 +27,10 @@ pub fn date_time_to_pspp(date_time: NaiveDateTime) -> f64 {
     (date_time - EPOCH_DATETIME).as_seconds_f64()
 }
 
+pub fn time_to_pspp(time: NaiveTime) -> f64 {
+    (time - NaiveTime::MIN).as_seconds_f64()
+}
+
 /// Takes a count of days from 14 Oct 1582 and translates it into a Gregorian
 /// calendar date, if possible.  Positive and negative offsets are supported.
 pub fn calendar_offset_to_gregorian(offset: f64) -> Option<NaiveDate> {
index 349657137bee3da778cd0efeca7a7248e70c79e4..db1a9e724b428d4e56d4db45bb98fe56fcc0e9cd 100644 (file)
@@ -806,8 +806,8 @@ impl BinWrite for Footnotes {
         endian: Endian,
         args: Self::Args<'_>,
     ) -> binrw::BinResult<()> {
-        (self.0.len() as u32).write_options(writer, endian, args)?;
-        for footnote in &self.0 {
+        (self.len() as u32).write_options(writer, endian, args)?;
+        for footnote in self {
             footnote.write_options(writer, endian, args)?;
         }
         Ok(())
index c2a5dc0b87a81181f7d7c2478de63d2a272eae1b..e0f5277d0419bb00017c3fc28fc3793c2049eef8 100644 (file)
@@ -59,7 +59,7 @@ use color::{AlphaColor, Rgba8, Srgb, palette::css::TRANSPARENT};
 use enum_iterator::Sequence;
 use enum_map::{Enum, EnumMap, enum_map};
 use itertools::Itertools;
-pub use look_xml::TableProperties;
+pub use look_xml::{Length, TableProperties};
 use quick_xml::{DeError, de::from_str};
 use serde::{
     Deserialize, Serialize, Serializer,
@@ -640,7 +640,7 @@ where
 }
 
 #[derive(Clone, Debug, Default, Serialize)]
-pub struct Footnotes(pub Vec<Arc<Footnote>>);
+pub struct Footnotes(Vec<Arc<Footnote>>);
 
 impl Footnotes {
     pub fn new() -> Self {
@@ -656,6 +656,32 @@ impl Footnotes {
     pub fn is_empty(&self) -> bool {
         self.0.is_empty()
     }
+
+    pub fn len(&self) -> usize {
+        self.0.len()
+    }
+
+    pub fn get(&self, index: usize) -> Option<&Arc<Footnote>> {
+        self.0.get(index)
+    }
+}
+
+impl Index<usize> for Footnotes {
+    type Output = Arc<Footnote>;
+
+    fn index(&self, index: usize) -> &Self::Output {
+        &self.0[index]
+    }
+}
+
+impl<'a> IntoIterator for &'a Footnotes {
+    type Item = &'a Arc<Footnote>;
+
+    type IntoIter = std::slice::Iter<'a, Arc<Footnote>>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.iter()
+    }
 }
 
 impl FromIterator<Footnote> for Footnotes {
@@ -812,7 +838,7 @@ impl From<&String> for Category {
 /// The division between this and the style information in [PivotTable] seems
 /// fairly arbitrary.  The ultimate reason for the division is simply because
 /// that's how SPSS documentation and file formats do it.
-#[derive(Clone, Debug, Serialize)]
+#[derive(Clone, Debug, PartialEq, Serialize)]
 pub struct Look {
     pub name: Option<String>,
 
@@ -1025,7 +1051,7 @@ impl From<Axis2> for HeadingRegion {
     }
 }
 
-#[derive(Clone, Debug, Serialize)]
+#[derive(Clone, Debug, PartialEq, Serialize)]
 pub struct AreaStyle {
     pub cell_style: CellStyle,
     pub font_style: FontStyle,
@@ -1267,7 +1293,7 @@ impl Display for DisplayCss {
     }
 }
 
-#[derive(Copy, Clone, Debug, Deserialize)]
+#[derive(Copy, Clone, Debug, PartialEq, Deserialize)]
 pub struct BorderStyle {
     #[serde(rename = "@borderStyleType")]
     pub stroke: Stroke,
@@ -2127,6 +2153,12 @@ impl Footnote {
     }
 }
 
+impl Default for Footnote {
+    fn default() -> Self {
+        Footnote::new(Value::default())
+    }
+}
+
 pub struct DisplayMarker<'a> {
     footnote: &'a Footnote,
     options: ValueOptions,
@@ -2290,14 +2322,15 @@ impl Value {
             Datum::String(string) => Self::new_user_text(string.as_str()),
         }
     }
-    pub fn new_variable_value(variable: &Variable, value: &Datum<ByteString>) -> Self {
-        let var_name = Some(variable.name.as_str().into());
-        let value_label = variable.value_labels.get(value).map(String::from);
+    pub fn new_datum_with_format<B>(value: &Datum<B>, format: Format) -> Self
+    where
+        B: EncodedString,
+    {
         match value {
             Datum::Number(number) => Self::new(ValueInner::Number(NumberValue {
                 show: None,
-                format: match variable.print_format.var_type() {
-                    VarType::Numeric => variable.print_format,
+                format: match format.var_type() {
+                    VarType::Numeric => format,
                     VarType::String => {
                         #[cfg(debug_assertions)]
                         panic!("cannot create numeric pivot value with string format");
@@ -2308,21 +2341,26 @@ impl Value {
                 },
                 honor_small: false,
                 value: *number,
-                variable: var_name,
-                value_label,
+                variable: None,
+                value_label: None,
             })),
             Datum::String(string) => Self::new(ValueInner::String(StringValue {
                 show: None,
-                hex: variable.print_format.type_() == Type::AHex,
-                s: string
-                    .as_ref()
-                    .with_encoding(variable.encoding())
-                    .into_string(),
-                var_name,
-                value_label,
+                hex: format.type_() == Type::AHex,
+                s: string.as_str().into_owned(),
+                var_name: None,
+                value_label: None,
             })),
         }
     }
+    pub fn new_variable_value(variable: &Variable, value: &Datum<ByteString>) -> Self {
+        Self::new_datum_with_format(
+            &value.as_encoded(variable.encoding()),
+            variable.print_format,
+        )
+        .with_variable_name(Some(variable.name.as_str().into()))
+        .with_value_label(variable.value_labels.get(value).map(String::from))
+    }
     pub fn new_number(x: Option<f64>) -> Self {
         Self::new_number_with_format(x, F8_2)
     }
index 2d338f6deb81532818558bf551cd79a079fd1c21..908c322458f026bfa91d2c9af3522920023baa76 100644 (file)
@@ -179,7 +179,7 @@ struct CellStyle {
     #[serde(rename = "@font-family")]
     font_family: String,
     #[serde(rename = "@font-size")]
-    font_size: Dimension,
+    font_size: Length,
     #[serde(rename = "@font-style")]
     font_style: FontStyle,
     #[serde(rename = "@font-weight")]
@@ -189,17 +189,17 @@ struct CellStyle {
     #[serde(rename = "@labelLocationVertical")]
     label_location_vertical: LabelLocationVertical,
     #[serde(rename = "@margin-bottom")]
-    margin_bottom: Dimension,
+    margin_bottom: Length,
     #[serde(rename = "@margin-left")]
-    margin_left: Dimension,
+    margin_left: Length,
     #[serde(rename = "@margin-right")]
-    margin_right: Dimension,
+    margin_right: Length,
     #[serde(rename = "@margin-top")]
-    margin_top: Dimension,
+    margin_top: Length,
     #[serde(rename = "@textAlignment", default)]
     text_alignment: TextAlignment,
     #[serde(rename = "@decimal-offset")]
-    decimal_offset: Dimension,
+    decimal_offset: Length,
 }
 
 impl CellStyle {
@@ -348,41 +348,41 @@ struct PrintingProperties {
 }
 
 #[derive(Copy, Clone, Default, PartialEq)]
-pub struct Dimension(
+pub struct Length(
     /// In inches.
-    f64,
+    pub f64,
 );
 
-impl Dimension {
-    fn as_px_f64(self) -> f64 {
+impl Length {
+    pub fn as_px_f64(self) -> f64 {
         self.0 * 96.0
     }
-    fn as_px_i32(self) -> i32 {
+    pub fn as_px_i32(self) -> i32 {
         num::cast(self.as_px_f64() + 0.5).unwrap_or_default()
     }
-    fn as_pt_f64(self) -> f64 {
+    pub fn as_pt_f64(self) -> f64 {
         self.0 * 72.0
     }
-    fn as_pt_i32(self) -> i32 {
+    pub fn as_pt_i32(self) -> i32 {
         num::cast(self.as_pt_f64() + 0.5).unwrap_or_default()
     }
 }
 
-impl Debug for Dimension {
+impl Debug for Length {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "{:.2}in", self.0)
     }
 }
 
-impl FromStr for Dimension {
-    type Err = DimensionParseError;
+impl FromStr for Length {
+    type Err = LengthParseError;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         let s = s.trim_start();
         let unit = s.trim_start_matches(|c: char| c.is_ascii_digit() || c == '.');
         let number: f64 = s[..s.len() - unit.len()]
             .parse()
-            .map_err(DimensionParseError::ParseFloatError)?;
+            .map_err(LengthParseError::ParseFloatError)?;
         let divisor = match unit.trim() {
             // Inches.
             "in" | "인치" | "pol." | "cala" | "cali" => 1.0,
@@ -396,14 +396,14 @@ impl FromStr for Dimension {
             // Centimeters.
             "cm" | "см" => 2.54,
 
-            other => return Err(DimensionParseError::InvalidUnit(other.into())),
+            other => return Err(LengthParseError::InvalidUnit(other.into())),
         };
-        Ok(Dimension(number / divisor))
+        Ok(Length(number / divisor))
     }
 }
 
 #[derive(ThisError, Debug, PartialEq, Eq)]
-enum DimensionParseError {
+pub enum LengthParseError {
     /// Invalid number.
     #[error(transparent)]
     ParseFloatError(ParseFloatError),
@@ -413,7 +413,7 @@ enum DimensionParseError {
     InvalidUnit(String),
 }
 
-impl<'de> Deserialize<'de> for Dimension {
+impl<'de> Deserialize<'de> for Length {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     where
         D: serde::Deserializer<'de>,
@@ -421,13 +421,13 @@ impl<'de> Deserialize<'de> for Dimension {
         struct DimensionVisitor;
 
         impl<'de> Visitor<'de> for DimensionVisitor {
-            type Value = Dimension;
+            type Value = Length;
 
             fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
-                formatter.write_str("a string")
+                formatter.write_str("a dimension expressed as a string, e.g. \"1.0 cm\"")
             }
 
-            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
+            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
             where
                 E: serde::de::Error,
             {
@@ -443,43 +443,49 @@ impl<'de> Deserialize<'de> for Dimension {
 mod tests {
     use std::str::FromStr;
 
+    use enum_map::{EnumMap, enum_map};
     use quick_xml::de::from_str;
 
-    use crate::output::pivot::look_xml::{Dimension, DimensionParseError, TableProperties};
+    use crate::output::pivot::{
+        Area, AreaStyle, Axis2, Border, BorderStyle, BoxBorder, CellStyle, Color, FontStyle,
+        FootnoteMarkerPosition, FootnoteMarkerType, HeadingRegion, HorzAlign, LabelPosition, Look,
+        RowColBorder, RowParity, Stroke, VertAlign,
+        look_xml::{Length, LengthParseError, TableProperties},
+    };
 
     #[test]
     fn dimension() {
-        assert_eq!(Dimension::from_str("1"), Ok(Dimension(1.0 / 72.0)));
-        assert_eq!(Dimension::from_str("1pt"), Ok(Dimension(1.0 / 72.0)));
-        assert_eq!(Dimension::from_str("1пт"), Ok(Dimension(1.0 / 72.0)));
-        assert_eq!(Dimension::from_str("1.0"), Ok(Dimension(1.0 / 72.0)));
-        assert_eq!(Dimension::from_str(" 1.0"), Ok(Dimension(1.0 / 72.0)));
-        assert_eq!(Dimension::from_str(" 1.0 "), Ok(Dimension(1.0 / 72.0)));
-        assert_eq!(Dimension::from_str("1.0 pt"), Ok(Dimension(1.0 / 72.0)));
-        assert_eq!(Dimension::from_str("1.0pt "), Ok(Dimension(1.0 / 72.0)));
-        assert_eq!(Dimension::from_str(" 1.0pt "), Ok(Dimension(1.0 / 72.0)));
+        assert_eq!(Length::from_str("1"), Ok(Length(1.0 / 72.0)));
+        assert_eq!(Length::from_str("1pt"), Ok(Length(1.0 / 72.0)));
+        assert_eq!(Length::from_str("1пт"), Ok(Length(1.0 / 72.0)));
+        assert_eq!(Length::from_str("1.0"), Ok(Length(1.0 / 72.0)));
+        assert_eq!(Length::from_str(" 1.0"), Ok(Length(1.0 / 72.0)));
+        assert_eq!(Length::from_str(" 1.0 "), Ok(Length(1.0 / 72.0)));
+        assert_eq!(Length::from_str("1.0 pt"), Ok(Length(1.0 / 72.0)));
+        assert_eq!(Length::from_str("1.0pt "), Ok(Length(1.0 / 72.0)));
+        assert_eq!(Length::from_str(" 1.0pt "), Ok(Length(1.0 / 72.0)));
 
-        assert_eq!(Dimension::from_str("1in"), Ok(Dimension(1.0)));
+        assert_eq!(Length::from_str("1in"), Ok(Length(1.0)));
 
-        assert_eq!(Dimension::from_str("96px"), Ok(Dimension(1.0)));
+        assert_eq!(Length::from_str("96px"), Ok(Length(1.0)));
 
-        assert_eq!(Dimension::from_str("2.54cm"), Ok(Dimension(1.0)));
+        assert_eq!(Length::from_str("2.54cm"), Ok(Length(1.0)));
 
         assert_eq!(
-            Dimension::from_str(""),
-            Err(DimensionParseError::ParseFloatError(
+            Length::from_str(""),
+            Err(LengthParseError::ParseFloatError(
                 "".parse::<f64>().unwrap_err()
             ))
         );
         assert_eq!(
-            Dimension::from_str("1.2.3"),
-            Err(DimensionParseError::ParseFloatError(
+            Length::from_str("1.2.3"),
+            Err(LengthParseError::ParseFloatError(
                 "1.2.3".parse::<f64>().unwrap_err()
             ))
         );
         assert_eq!(
-            Dimension::from_str("1asdf"),
-            Err(DimensionParseError::InvalidUnit("asdf".into()))
+            Length::from_str("1asdf"),
+            Err(LengthParseError::InvalidUnit("asdf".into()))
         );
     }
 
@@ -541,7 +547,418 @@ mod tests {
 </tableProperties>
 "##;
         let table_properties: TableProperties = from_str(XML).unwrap();
-        dbg!(&table_properties);
-        todo!()
+        let look: Look = table_properties.into();
+        dbg!(&look);
+        let expected = Look {
+            name: None,
+            hide_empty: true,
+            row_label_position: LabelPosition::Corner,
+            heading_widths: enum_map! {
+                HeadingRegion::Rows => 36..=120,
+                HeadingRegion::Columns => 36..=72,
+            },
+            footnote_marker_type: FootnoteMarkerType::Alphabetic,
+            footnote_marker_position: FootnoteMarkerPosition::Subscript,
+            areas: enum_map! {
+                Area::Title => AreaStyle {
+                    cell_style: CellStyle {
+                        horz_align: Some(
+                            HorzAlign::Left,
+                        ),
+                        vert_align: VertAlign::Middle,
+                        margins: enum_map! {
+                            Axis2::X => [
+                                8,
+                                11,
+                            ],
+                            Axis2::Y => [
+                                0,
+                                8,
+                            ],
+                        },
+                    },
+                    font_style: FontStyle {
+                        bold: true,
+                        italic: false,
+                        underline: false,
+                        markup: false,
+                        font: String::from("Sans Serif"),
+                        fg: Color::BLACK,
+                        bg: Color::WHITE,
+                        size: 9,
+                    },
+                },
+                Area::Caption => AreaStyle {
+                    cell_style: CellStyle {
+                        horz_align: Some(
+                            HorzAlign::Left,
+                        ),
+                        vert_align: VertAlign::Top,
+                        margins: enum_map! {
+                            Axis2::X => [
+                                8,
+                                11,
+                            ],
+                            Axis2::Y => [
+                                0,
+                                0,
+                            ],
+                        },
+                    },
+                    font_style: FontStyle {
+                        bold: false,
+                        italic: false,
+                        underline: false,
+                        markup: false,
+                        font: String::from("Sans Serif"),
+                        fg: Color::BLACK,
+                        bg: Color::WHITE,
+                        size: 9,
+                    },
+                },
+                Area::Footer => AreaStyle {
+                    cell_style: CellStyle {
+                        horz_align: Some(
+                            HorzAlign::Left,
+                        ),
+                        vert_align: VertAlign::Top,
+                        margins: enum_map! {
+                            Axis2::X => [
+                                11,
+                                8,
+                            ],
+                            Axis2::Y => [
+                                1,
+                                3,
+                            ],
+                        },
+                    },
+                    font_style: FontStyle {
+                        bold: false,
+                        italic: false,
+                        underline: false,
+                        markup: false,
+                        font: String::from("Sans Serif"),
+                        fg: Color::BLACK,
+                        bg: Color::WHITE,
+                        size: 9,
+                    },
+                },
+                Area::Corner => AreaStyle {
+                    cell_style: CellStyle {
+                        horz_align: Some(
+                            HorzAlign::Left,
+                        ),
+                        vert_align: VertAlign::Bottom,
+                        margins: enum_map! {
+                            Axis2::X => [
+                                8,
+                                11,
+                            ],
+                            Axis2::Y => [
+                                0,
+                                0,
+                            ],
+                        },
+                    },
+                    font_style: FontStyle {
+                        bold: false,
+                        italic: false,
+                        underline: false,
+                        markup: false,
+                        font: String::from("Sans Serif"),
+                        fg: Color::BLACK,
+                        bg: Color::WHITE,
+                        size: 9,
+                    },
+                },
+                Area::Labels(
+                    Axis2::X,
+                ) => AreaStyle {
+                    cell_style: CellStyle {
+                        horz_align: Some(
+                            HorzAlign::Center,
+                        ),
+                        vert_align: VertAlign::Bottom,
+                        margins: enum_map! {
+                            Axis2::X => [
+                                8,
+                                11,
+                            ],
+                            Axis2::Y => [
+                                0,
+                                3,
+                            ],
+                        },
+                    },
+                    font_style: FontStyle {
+                        bold: false,
+                        italic: false,
+                        underline: false,
+                        markup: false,
+                        font: String::from("Sans Serif"),
+                        fg: Color::BLACK,
+                        bg: Color::WHITE,
+                        size: 9,
+                    },
+                },
+                Area::Labels(
+                    Axis2::Y,
+                )=> AreaStyle {
+                    cell_style: CellStyle {
+                        horz_align: Some(
+                            HorzAlign::Left,
+                        ),
+                        vert_align: VertAlign::Top,
+                        margins: enum_map! {
+                            Axis2::X => [
+                                8,
+                                11,
+                            ],
+                            Axis2::Y => [
+                                0,
+                                3,
+                            ],
+                        },
+                    },
+                    font_style: FontStyle {
+                        bold: false,
+                        italic: false,
+                        underline: false,
+                        markup: false,
+                        font: String::from("Sans Serif"),
+                        fg: Color::BLACK,
+                        bg: Color::WHITE,
+                        size: 9,
+                    },
+                },
+                Area::Data(
+                    RowParity::Even,
+                ) => AreaStyle {
+                    cell_style: CellStyle {
+                        horz_align: None,
+                        vert_align: VertAlign::Top,
+                        margins: enum_map! {
+                            Axis2::X => [
+                                8,
+                                11,
+                            ],
+                            Axis2::Y => [
+                                0,
+                                0,
+                            ],
+                        },
+                    },
+                    font_style: FontStyle {
+                        bold: false,
+                        italic: false,
+                        underline: false,
+                        markup: false,
+                        font: String::from("Sans Serif"),
+                        fg: Color::BLACK,
+                        bg: Color::WHITE,
+                        size: 9,
+                    },
+                },
+                Area::Data(
+                    RowParity::Odd,
+                )=>AreaStyle {
+                    cell_style: CellStyle {
+                        horz_align: None,
+                        vert_align: VertAlign::Top,
+                        margins: enum_map! {
+                            Axis2::X => [
+                                8,
+                                11,
+                            ],
+                            Axis2::Y => [
+                                0,
+                                0,
+                            ],
+                        },
+                    },
+                    font_style: FontStyle {
+                        bold: false,
+                        italic: false,
+                        underline: false,
+                        markup: false,
+                        font: String::from("Sans Serif"),
+                        fg: Color::BLACK,
+                        bg: Color::BLACK,
+                        size: 9,
+                    },
+                },
+                Area::Layers => AreaStyle {
+                    cell_style: CellStyle {
+                        horz_align: Some(
+                            HorzAlign::Left,
+                        ),
+                        vert_align: VertAlign::Bottom,
+                        margins: enum_map! {
+                            Axis2::X => [
+                                8,
+                                11,
+                            ],
+                            Axis2::Y => [
+                                0,
+                                3,
+                            ],
+                        },
+                    },
+                    font_style: FontStyle {
+                        bold: false,
+                        italic: false,
+                        underline: false,
+                        markup: false,
+                        font: String::from("Sans Serif"),
+                        fg: Color::BLACK,
+                        bg: Color::WHITE,
+                        size: 9,
+                    },
+                },
+            },
+            borders: enum_map! {
+                Border::Title => BorderStyle {
+                    stroke: Stroke::None,
+                    color: Color::BLACK,
+                },
+                Border::OuterFrame(
+                    BoxBorder::Left,
+                )=>BorderStyle {
+                    stroke: Stroke::None,
+                    color: Color::BLACK,
+                },
+                Border::OuterFrame(
+                    BoxBorder::Top,
+                ) =>BorderStyle {
+                    stroke: Stroke::None,
+                    color: Color::BLACK,
+                },
+                Border::OuterFrame(
+                    BoxBorder::Right,
+                ) => BorderStyle {
+                    stroke: Stroke::None,
+                    color: Color::BLACK,
+                },
+                Border::OuterFrame(
+                    BoxBorder::Bottom,
+                )=> BorderStyle {
+                    stroke: Stroke::None,
+                    color: Color::BLACK,
+                },
+                Border::InnerFrame(
+                    BoxBorder::Left,
+                )=> BorderStyle {
+                    stroke: Stroke::Thick,
+                    color: Color::BLACK,
+                },
+                Border::InnerFrame(
+                    BoxBorder::Top,
+                )=> BorderStyle {
+                    stroke: Stroke::Thick,
+                    color: Color::BLACK,
+                },
+                Border::InnerFrame(
+                    BoxBorder::Right,
+                )=> BorderStyle {
+                    stroke: Stroke::Thick,
+                    color: Color::BLACK,
+                },
+                Border::InnerFrame(
+                    BoxBorder::Bottom,
+                )=> BorderStyle {
+                    stroke: Stroke::Thick,
+                    color: Color::BLACK,
+                },
+                Border::Dimension(
+                    RowColBorder(
+                        HeadingRegion::Rows,
+                        Axis2::X,
+                    ),
+                )=> BorderStyle {
+                    stroke: Stroke::Solid,
+                    color: Color::BLACK,
+                },
+                Border::Dimension(
+                    RowColBorder(
+                        HeadingRegion::Columns,
+                        Axis2::X,
+                    ),
+                )=> BorderStyle {
+                    stroke: Stroke::Solid,
+                    color: Color::BLACK,
+                },
+                Border::Dimension(
+                    RowColBorder(
+                        HeadingRegion::Rows,
+                        Axis2::Y,
+                    ),
+                )=> BorderStyle {
+                    stroke: Stroke::None,
+                    color: Color::BLACK,
+                },
+                Border::Dimension(
+                    RowColBorder(
+                        HeadingRegion::Columns,
+                        Axis2::Y,
+                    ),
+                )=> BorderStyle {
+                    stroke: Stroke::Solid,
+                    color: Color::BLACK,
+                },
+                Border::Category(
+                    RowColBorder(
+                        HeadingRegion::Rows,
+                        Axis2::X,
+                    ),
+                )=> BorderStyle {
+                    stroke: Stroke::None,
+                    color: Color::BLACK,
+                },
+                Border::Category(
+                    RowColBorder(
+                        HeadingRegion::Columns,
+                        Axis2::X,
+                    ),
+                )=> BorderStyle {
+                    stroke: Stroke::Solid,
+                    color: Color::BLACK,
+                },
+                Border::Category(
+                    RowColBorder(
+                        HeadingRegion::Rows,
+                        Axis2::Y,
+                    ),
+                )=> BorderStyle {
+                    stroke: Stroke::None,
+                    color: Color::BLACK,
+                },
+                Border::Category(
+                    RowColBorder(
+                        HeadingRegion::Columns,
+                        Axis2::Y,
+                    ),
+                )=> BorderStyle {
+                    stroke: Stroke::Solid,
+                    color: Color::BLACK,
+                },
+                Border::DataLeft => BorderStyle {
+                    stroke: Stroke::Thick,
+                    color: Color::BLACK,
+                },
+                Border::DataTop => BorderStyle {
+                    stroke: Stroke::Thick,
+                    color: Color::BLACK,
+                },
+            },
+            print_all_layers: true,
+            paginate_layers: false,
+            shrink_to_fit: EnumMap::from_fn(|_| false),
+            top_continuation: false,
+            bottom_continuation: false,
+            continuation: None,
+            n_orphan_lines: 5,
+        };
+        assert_eq!(&look, &expected);
     }
 }
index 2b5acb0003aa62e4d3fc8c7cde2eefa1aeb7bd9c..b47911c6d230e2333b763a79536c806da419210f 100644 (file)
@@ -443,12 +443,14 @@ impl Table {
                     Ok(result) => result,
                     Err(error) => panic!("{error:?}"),
                 };
-                visualization.decode(
-                    data,
-                    self.properties
-                        .as_ref()
-                        .map_or_else(Look::default, |properties| properties.clone().into()),
-                );
+                visualization
+                    .decode(
+                        data,
+                        self.properties
+                            .as_ref()
+                            .map_or_else(Look::default, |properties| properties.clone().into()),
+                    )
+                    .unwrap()/*XXX*/;
 
                 Ok(PivotTable::new([]).into_item())
             }
index cc298eee7ca1aafa359eb747a45d780d5c2aaef5..8af6c7f6fcc6b01b535cbbe7a9923097b3f1c48d 100644 (file)
@@ -4,11 +4,17 @@ use std::{
 };
 
 use binrw::{BinRead, BinResult, binread};
+use chrono::{NaiveDateTime, NaiveTime};
 use encoding_rs::UTF_8;
 
 use crate::{
+    calendar::{date_time_to_pspp, time_to_pspp},
     data::Datum,
-    output::spv::light::{U32String, parse_vec},
+    format::{Category, Format},
+    output::{
+        pivot::Value,
+        spv::light::{U32String, decode_format, parse_vec},
+    },
 };
 
 #[binread]
@@ -76,6 +82,45 @@ pub struct DataValue {
     pub value: Datum<String>,
 }
 
+impl DataValue {
+    pub fn category(&self) -> Option<usize> {
+        match &self.value {
+            Datum::Number(number) => *number,
+            _ => self.index,
+        }
+        .and_then(|v| (v >= 0.0 && v < usize::MAX as f64).then_some(v as usize))
+    }
+
+    pub fn as_format(&self, format_map: &HashMap<u32, Format>) -> Format {
+        let f = match &self.value {
+            Datum::Number(Some(number)) => *number as u32,
+            Datum::Number(None) => 0,
+            Datum::String(s) => s.parse().unwrap_or_default(),
+        };
+        match format_map.get(&f) {
+            Some(format) => *format,
+            None => decode_format(f),
+        }
+    }
+
+    pub fn as_pivot_value(&self, format: Format) -> Value {
+        if format.type_().category() == Category::Date
+            && let Some(s) = self.value.as_string()
+            && let Ok(date_time) =
+                NaiveDateTime::parse_from_str(s.as_str(), "%Y-%m-%dT%H:%M:%S%.3f")
+        {
+            Value::new_number_with_format(Some(date_time_to_pspp(date_time)), format)
+        } else if format.type_().category() == Category::Time
+            && let Some(s) = self.value.as_string()
+            && let Ok(time) = NaiveTime::parse_from_str(s.as_str(), "%H:%M:%S%.3f")
+        {
+            Value::new_number_with_format(Some(time_to_pspp(time)), format)
+        } else {
+            Value::new_datum_with_format(&self.value, format)
+        }
+    }
+}
+
 #[binread]
 #[br(little)]
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
index f0ca5803b87706b18a04e189bd9ac25674d6726f..db02c0f5c79eee5c5eed394fcbe68d42a8c32c76 100644 (file)
@@ -18,26 +18,23 @@ use std::{
     collections::{BTreeMap, HashMap},
     marker::PhantomData,
     mem::take,
-    num::{NonZeroUsize, ParseFloatError},
-    str::FromStr,
+    num::NonZeroUsize,
 };
 
 use enum_map::{Enum, EnumMap};
-use itertools::Itertools;
 use ordered_float::OrderedFloat;
-use serde::{Deserialize, de::Error as _};
+use serde::Deserialize;
 
 use crate::{
     data::Datum,
     format::{Decimal::Dot, F8_0, Type, UncheckedFormat},
     output::{
         pivot::{
-            self, Area, AreaStyle, Axis2, Axis3, Color, HeadingRegion, HorzAlign, Look, PivotTable,
-            RowParity, Value, VertAlign,
+            self, Area, AreaStyle, Axis2, Axis3, Color, HeadingRegion, HorzAlign, Leaf, Length,
+            Look, PivotTable, RowParity, Value, VertAlign,
         },
         spv::legacy_bin::DataValue,
     },
-    variable,
 };
 
 #[derive(Debug)]
@@ -192,7 +189,7 @@ impl Visualization {
         let mut graph = None;
         let mut labels = EnumMap::from_fn(|_| Vec::new());
         let mut styles = HashMap::new();
-        let mut layer_controller = None;
+        let mut _layer_controller = None;
         for child in &self.children {
             match child {
                 VisChild::Extension(e) => extension = Some(e),
@@ -224,11 +221,11 @@ impl Visualization {
                         styles.insert(id.as_str(), style);
                     }
                 }
-                VisChild::LayerController(lc) => layer_controller = Some(lc),
+                VisChild::LayerController(lc) => _layer_controller = Some(lc),
             }
         }
         let Some(graph) = graph else { todo!() };
-        let Some(user_source) = user_source else {
+        let Some(_user_source) = user_source else {
             todo!()
         };
 
@@ -241,37 +238,51 @@ impl Visualization {
 
         // Footnotes.
         //
-        // Any pivot_value might refer to footnotes, so it's important to
-        // process the footnotes early to ensure that those references can be
-        // resolved.  There is a possible problem that a footnote might itself
-        // reference an as-yet-unprocessed footnote, but that's OK because
-        // footnote references don't actually look at the footnote contents but
-        // only resolve a pointer to where the footnote will go later.
+        // Any [Value] might refer to footnotes, so it's important to process
+        // the footnotes early to ensure that those references can be resolved.
+        // There is a possible problem that a footnote might itself reference an
+        // as-yet-unprocessed footnote, but that's OK because footnote
+        // references don't actually look at the footnote contents but only
+        // resolve a pointer to where the footnote will go later.
         //
         // Before we really start, create all the footnotes we'll fill in.  This
         // is because sometimes footnotes refer to themselves or to each other
         // and we don't want to reject those references.
-        let mut footnotes = BTreeMap::<usize, (String, String)>::new();
+        let mut footnote_builder = BTreeMap::<usize, Footnote>::new();
         if let Some(f) = &graph.interval.footnotes {
-            f.decode(&mut footnotes);
+            f.decode(&mut footnote_builder);
         }
         for child in &graph.interval.labeling.children {
             if let LabelingChild::Footnotes(f) = child {
-                f.decode(&mut footnotes);
+                f.decode(&mut footnote_builder);
             }
         }
         for label in &labels[Purpose::Footnote] {
             for (index, text) in label.text().iter().enumerate() {
                 if let Some(uses_reference) = text.uses_reference {
-                    let entry = footnotes.entry(uses_reference.get() - 1).or_default();
+                    let entry = footnote_builder
+                        .entry(uses_reference.get() - 1)
+                        .or_default();
                     if index % 2 == 0 {
-                        entry.0 = text.text.strip_suffix('\n').unwrap_or(&text.text).into();
+                        entry.content = text.text.strip_suffix('\n').unwrap_or(&text.text).into();
                     } else {
-                        entry.1 = text.text.strip_suffix('.').unwrap_or(&text.text).into();
+                        entry.marker =
+                            Some(text.text.strip_suffix('.').unwrap_or(&text.text).into());
                     }
                 }
             }
         }
+        let mut footnotes = Vec::new();
+        for (index, footnote) in footnote_builder {
+            while footnotes.len() < index {
+                footnotes.push(pivot::Footnote::default());
+            }
+            footnotes.push(
+                pivot::Footnote::new(footnote.content)
+                    .with_marker(footnote.marker.map(|s| Value::new_user_text(s))),
+            );
+        }
+        let footnotes = pivot::Footnotes::from_iter(footnotes);
 
         for (purpose, area) in [
             (Purpose::Title, Area::Title),
@@ -295,11 +306,11 @@ impl Visualization {
                 look.areas[Area::Data(RowParity::Even)].clone();
         }
 
-        let mut title = Value::empty();
-        let mut caption = Value::empty();
+        let mut _title = Value::empty();
+        let mut _caption = Value::empty();
         //Label::decode_
 
-        let show_grid_lines = extension
+        let _show_grid_lines = extension
             .as_ref()
             .and_then(|extension| extension.show_gridline);
         if let Some(style) = styles.get(graph.cell_style.references.as_str())
@@ -309,11 +320,11 @@ impl Visualization {
             parts.next();
             if let Some(min_width) = parts.next()
                 && let Some(max_width) = parts.next()
-                && let Ok(min_width) = min_width.parse::<Dimension>()
-                && let Ok(max_width) = max_width.parse::<Dimension>()
+                && let Ok(min_width) = min_width.parse::<Length>()
+                && let Ok(max_width) = max_width.parse::<Length>()
             {
                 look.heading_widths[HeadingRegion::Columns] =
-                    min_width.as_pt() as usize..=max_width.as_pt() as usize;
+                    min_width.as_pt_f64() as usize..=max_width.as_pt_f64() as usize;
             }
         }
 
@@ -351,6 +362,9 @@ impl Visualization {
             styles: &HashMap<&str, &Style>,
             a: Axis3,
             look: &mut Look,
+            rotate_inner_column_labels: &mut bool,
+            rotate_outer_row_labels: &mut bool,
+            footnotes: &pivot::Footnotes,
         ) {
             let base_level = variables[0].1;
             if let Ok(a) = Axis2::try_from(a)
@@ -367,7 +381,57 @@ impl Visualization {
             }
             if a == Axis3::Y
                 && let Some(axis) = axes.get(&(base_level + variables.len() - 1))
-            {}
+            {
+                Style::decode(
+                    axis.major_ticks.style.get(&styles),
+                    axis.major_ticks.tick_frame_style.get(&styles),
+                    &mut look.areas[Area::Labels(Axis2::Y)],
+                );
+            }
+
+            if let Some(axis) = axes.get(&base_level)
+                && axis.major_ticks.label_angle == -90.0
+            {
+                if a == Axis3::X {
+                    *rotate_inner_column_labels = true;
+                } else {
+                    *rotate_outer_row_labels = true;
+                }
+            }
+
+            // Find the first row for each category.
+            let max_cat = variables[0].0.max_category().unwrap()/*XXX*/;
+            let mut cat_rows = vec![None; max_cat + 1];
+            for (index, value) in variables[0].0.values.iter().enumerate() {
+                if let Some(row) = value.category() {
+                    cat_rows[row].get_or_insert(index);
+                }
+            }
+
+            // Drop missing categories and count what's left.
+            let cat_rows = cat_rows.into_iter().flatten().collect::<Vec<_>>();
+
+            // Make leaf categories.
+            let mut cats = Vec::with_capacity(cat_rows.len());
+            for row in cat_rows.iter().copied() {
+                let dv = &variables[0].0.values[row];
+                let name = Value::new_datum(&dv.value);
+                let name = variables[0].0.add_affixes(name, &footnotes);
+                cats.push(Leaf::new(name));
+            }
+
+            // Now group them, in one pass per grouping variable, innermost first.
+            for j in 1..variables.len() {
+                // Find a sequence of categories `cat1...cat2`, that all have
+                // the same value in series `j`.  (This might be only a single
+                // category.) */
+                let series = variables[j].0;
+                let mut cat1 = 0;
+                while cat1 < cats.len() {
+                    let mut cat2 = cat1 + 1;
+                    while cat2 < cats.len() {}
+                }
+            }
 
             todo!()
         }
@@ -379,6 +443,10 @@ impl Visualization {
             styles: &HashMap<&str, &Style>,
             a: Axis3,
             look: &mut Look,
+            rotate_inner_column_labels: &mut bool,
+            rotate_outer_row_labels: &mut bool,
+            footnotes: &pivot::Footnotes,
+
             level_ofs: usize,
         ) -> Vec<pivot::Dimension> {
             let variables = variables
@@ -396,22 +464,53 @@ impl Visualization {
                 if let Some((var, level)) = var {
                     dim_vars.push((var, level));
                 } else if !dim_vars.is_empty() {
-                    decode_dimension(&dim_vars, axes, styles, a, look);
+                    decode_dimension(
+                        &dim_vars,
+                        axes,
+                        styles,
+                        a,
+                        look,
+                        rotate_inner_column_labels,
+                        rotate_outer_row_labels,
+                        footnotes,
+                    );
                     dim_vars.clear();
                 }
             }
             if !dim_vars.is_empty() {
-                decode_dimension(&dim_vars, axes, styles, a, look);
+                decode_dimension(
+                    &dim_vars,
+                    axes,
+                    styles,
+                    a,
+                    look,
+                    rotate_inner_column_labels,
+                    rotate_outer_row_labels,
+                    footnotes,
+                );
             }
             todo!()
         }
 
+        let mut rotate_inner_column_labels = false;
+        let mut rotate_outer_row_labels = false;
         let cross = &graph.faceting.cross.children;
         let columns = cross
             .first()
             .map(|child| child.variables())
             .unwrap_or_default();
-        decode_dimensions(columns, &series, &axes, &styles, Axis3::X, &mut look, 1);
+        decode_dimensions(
+            columns,
+            &series,
+            &axes,
+            &styles,
+            Axis3::X,
+            &mut look,
+            &mut rotate_inner_column_labels,
+            &mut rotate_outer_row_labels,
+            &footnotes,
+            1,
+        );
         let rows = cross
             .get(1)
             .map(|child| child.variables())
@@ -423,6 +522,9 @@ impl Visualization {
             &styles,
             Axis3::Y,
             &mut look,
+            &mut rotate_inner_column_labels,
+            &mut rotate_outer_row_labels,
+            &footnotes,
             1 + columns.len(),
         );
 
@@ -439,6 +541,27 @@ struct Series {
     affixes: Vec<Affix>,
 }
 
+impl Series {
+    fn add_affixes(&self, mut value: Value, footnotes: &pivot::Footnotes) -> Value {
+        for affix in &self.affixes {
+            if let Some(index) = affix.defines_reference.checked_sub(1)
+                && let Ok(index) = usize::try_from(index)
+                && let Some(footnote) = footnotes.get(index)
+            {
+                value = value.with_footnote(footnote);
+            }
+        }
+        value
+    }
+
+    fn max_category(&self) -> Option<usize> {
+        self.values
+            .iter()
+            .filter_map(|value| value.category())
+            .max()
+    }
+}
+
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 enum VisChild {
@@ -1143,57 +1266,6 @@ struct ValueMapEntry {
     to: String,
 }
 
-#[derive(Copy, Clone, Default, Debug, PartialEq, PartialOrd)]
-struct Dimension(f64);
-
-impl Dimension {
-    fn as_px(&self) -> f64 {
-        self.0 * 96.0
-    }
-    fn as_pt(&self) -> f64 {
-        self.0 * 72.0
-    }
-}
-
-impl<'de> Deserialize<'de> for Dimension {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        let string = String::deserialize(deserializer)?;
-        Dimension::from_str(&string).map_err(D::Error::custom)
-    }
-}
-
-impl FromStr for Dimension {
-    type Err = ParseFloatError;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        fn parse_unit(s: &str) -> (f64, &str) {
-            for (unit, per_inch) in &[
-                ("in", 1.0),
-                ("인치", 1.0),
-                ("pol.", 1.0),
-                ("cala", 1.0),
-                ("cali", 1.0),
-                ("cm", 2.54),
-                ("см", 2.54),
-                ("pt", 72.0),
-                ("пт", 72.0),
-                ("px", 96.0),
-            ] {
-                if let Some(rest) = s.strip_suffix(unit) {
-                    return (*per_inch, rest);
-                }
-            }
-            (72.0, s)
-        }
-
-        let (per_inch, s) = parse_unit(s);
-        Ok(Self(s.trim().parse::<f64>()? / per_inch))
-    }
-}
-
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 struct Style {
@@ -1253,16 +1325,16 @@ struct Style {
     font_underline: Option<FontUnderline>,
 
     #[serde(rename = "@margin-bottom")]
-    margin_bottom: Option<Dimension>,
+    margin_bottom: Option<Length>,
 
     #[serde(rename = "@margin-top")]
-    margin_top: Option<Dimension>,
+    margin_top: Option<Length>,
 
     #[serde(rename = "@margin-left")]
-    margin_left: Option<Dimension>,
+    margin_left: Option<Length>,
 
     #[serde(rename = "@margin-right")]
-    margin_right: Option<Dimension>,
+    margin_right: Option<Length>,
 
     #[serde(rename = "@textAlignment")]
     text_alignment: Option<TextAlignment>,
@@ -1283,7 +1355,7 @@ struct Style {
     visible: Option<bool>,
 
     #[serde(rename = "@decimal-offset")]
-    decimal_offset: Option<Dimension>,
+    decimal_offset: Option<Length>,
 }
 
 impl Style {
@@ -1386,13 +1458,13 @@ enum TextAlignment {
 }
 
 impl TextAlignment {
-    fn as_horz_align(&self, decimal_offset: Option<Dimension>) -> Option<HorzAlign> {
+    fn as_horz_align(&self, decimal_offset: Option<Length>) -> Option<HorzAlign> {
         match self {
             TextAlignment::Left => Some(HorzAlign::Left),
             TextAlignment::Right => Some(HorzAlign::Right),
             TextAlignment::Center => Some(HorzAlign::Center),
             TextAlignment::Decimal => Some(HorzAlign::Decimal {
-                offset: decimal_offset.unwrap_or_default().as_px(),
+                offset: decimal_offset.unwrap_or_default().as_px_f64(),
                 decimal: Dot,
             }),
             TextAlignment::Mixed => None,
@@ -1452,11 +1524,11 @@ struct Location {
 
     /// Minimum size.
     #[serde(rename = "@min")]
-    min: Option<Dimension>,
+    min: Option<Length>,
 
     /// Maximum size.
     #[serde(rename = "@max")]
-    max: Option<Dimension>,
+    max: Option<Length>,
 
     /// An element to attach to. Required when method is attach or same, not
     /// observed otherwise.
@@ -1749,6 +1821,12 @@ struct FormatMapping {
     format: Option<Format>,
 }
 
+#[derive(Clone, Debug, Default)]
+struct Footnote {
+    content: String,
+    marker: Option<String>,
+}
+
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 struct Footnotes {
@@ -1763,9 +1841,11 @@ struct Footnotes {
 }
 
 impl Footnotes {
-    fn decode(&self, dst: &mut BTreeMap<usize, (String, String)>) {
+    fn decode(&self, dst: &mut BTreeMap<usize, Footnote>) {
         for f in &self.mappings {
-            dst.entry(f.defines_reference.get() - 1).or_default().0 = f.to.clone();
+            dst.entry(f.defines_reference.get() - 1)
+                .or_default()
+                .content = f.to.clone();
         }
     }
 }
@@ -1793,7 +1873,7 @@ struct FacetLevel {
     level: usize,
 
     #[serde(rename = "@gap")]
-    gap: Option<Dimension>,
+    gap: Option<Length>,
     axis: Axis,
 }
 
@@ -1817,7 +1897,7 @@ struct MajorTicks {
     label_angle: f64,
 
     #[serde(rename = "@length")]
-    length: Dimension,
+    length: Length,
 
     #[serde(rename = "@style")]
     style: Ref<Style>,
@@ -2011,25 +2091,3 @@ struct LayerController {
     #[serde(rename = "@target")]
     target: Option<Ref<Label>>,
 }
-
-#[cfg(test)]
-mod tests {
-    use std::str::FromStr;
-
-    use crate::output::spv::legacy_xml::Dimension;
-
-    #[test]
-    fn dimension() {
-        for s in [
-            "1in",
-            "1.0인치",
-            "1.0e0 cali",
-            "96px",
-            "72 ",
-            "72 пт",
-            " 2.54см",
-        ] {
-            assert_eq!(Dimension(1.0), Dimension::from_str(s).unwrap());
-        }
-    }
-}
index 217e36993aa9a23aa64d980fbfefaa84663b7e59..903e48ecefb503407befcf22e082e67dc9064d77 100644 (file)
@@ -17,10 +17,14 @@ use enum_map::{EnumMap, enum_map};
 
 use crate::{
     format::{
-        Decimal, Decimals, Epoch, Format, NumberStyle, Settings, Type, UncheckedFormat, Width, CC, F40
+        CC, Decimal, Decimals, Epoch, F40, Format, NumberStyle, Settings, Type, UncheckedFormat,
+        Width,
     },
     output::pivot::{
-        self, parse_bool, AreaStyle, Axis2, Axis3, BoxBorder, Color, FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Look, PivotTable, PivotTableMetadata, PivotTableStyle, PrecomputedIndex, RowColBorder, RowParity, StringValue, Stroke, TemplateValue, ValueStyle, VariableValue, VertAlign
+        self, AreaStyle, Axis2, Axis3, BoxBorder, Color, FootnoteMarkerPosition,
+        FootnoteMarkerType, Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Look,
+        PivotTable, PivotTableMetadata, PivotTableStyle, PrecomputedIndex, RowColBorder, RowParity,
+        StringValue, Stroke, TemplateValue, ValueStyle, VariableValue, VertAlign, parse_bool,
     },
     settings::Show,
 };
@@ -1180,11 +1184,9 @@ impl BinRead for Value {
     }
 }
 
-#[binrw::parser(reader, endian)]
-fn parse_format() -> BinResult<Format> {
-    let raw = u32::read_options(reader, endian, ())?;
+pub(super) fn decode_format(raw: u32) -> Format {
     if raw == 0 || raw == 0x10000 || raw == 1 {
-        return Ok(Format::new(Type::F, 40, 2).unwrap());
+        return Format::new(Type::F, 40, 2).unwrap();
     }
 
     let raw_type = (raw >> 16) as u16;
@@ -1199,7 +1201,12 @@ fn parse_format() -> BinResult<Format> {
     let w = ((raw >> 8) & 0xff) as Width;
     let d = raw as Decimals;
 
-    Ok(UncheckedFormat::new(type_, w, d).fix())
+    UncheckedFormat::new(type_, w, d).fix()
+}
+
+#[binrw::parser(reader, endian)]
+fn parse_format() -> BinResult<Format> {
+    Ok(decode_format(u32::read_options(reader, endian, ())?))
 }
 
 impl ValueNumber {
@@ -1429,7 +1436,7 @@ impl ValueMods {
             footnotes: self
                 .refs
                 .iter()
-                .flat_map(|index| footnotes.0.get(*index as usize))
+                .flat_map(|index| footnotes.get(*index as usize))
                 .cloned()
                 .collect(),
         }