work
authorBen Pfaff <blp@cs.stanford.edu>
Mon, 19 Aug 2024 01:32:16 +0000 (18:32 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Mon, 19 Aug 2024 01:32:16 +0000 (18:32 -0700)
rust/src/cooked.rs
rust/src/dictionary.rs
rust/src/endian.rs
rust/src/format.rs
rust/src/lib.rs
rust/src/message.rs
rust/src/output/pivot/mod.rs
rust/src/settings.rs [new file with mode: 0644]

index 96468531c28c015b3a911ba47137787bf5ea06a7..d2617df52897268c7a850329170b1e548b949571 100644 (file)
@@ -4,7 +4,7 @@ use crate::{
     dictionary::{Dictionary, VarWidth, Variable},
     encoding::Error as EncodingError,
     endian::Endian,
-    format::{Error as FormatError, Spec, UncheckedSpec},
+    format::{Error as FormatError, Format, UncheckedFormat},
     identifier::{Error as IdError, Identifier},
     raw::{
         self, Cases, DecodedRecord, DocumentRecord, EncodingRecord, Extension, FileAttributeRecord,
@@ -59,7 +59,7 @@ pub enum Error {
         "Substituting {new_spec} for invalid print format on variable {variable}.  {format_error}"
     )]
     InvalidPrintFormat {
-        new_spec: Spec,
+        new_spec: Format,
         variable: Identifier,
         format_error: FormatError,
     },
@@ -68,7 +68,7 @@ pub enum Error {
         "Substituting {new_spec} for invalid write format on variable {variable}.  {format_error}"
     )]
     InvalidWriteFormat {
-        new_spec: Spec,
+        new_spec: Format,
         variable: Identifier,
         format_error: FormatError,
     },
@@ -584,12 +584,12 @@ fn fix_line_ends(s: &str) -> String {
     out
 }
 
-fn decode_format(raw: raw::Spec, width: VarWidth, warn: impl Fn(Spec, FormatError)) -> Spec {
-    UncheckedSpec::try_from(raw)
-        .and_then(Spec::try_from)
+fn decode_format(raw: raw::Spec, width: VarWidth, warn: impl Fn(Format, FormatError)) -> Format {
+    UncheckedFormat::try_from(raw)
+        .and_then(Format::try_from)
         .and_then(|x| x.check_width_compatibility(width))
         .unwrap_or_else(|error| {
-            let new_format = Spec::default_for_width(width);
+            let new_format = Format::default_for_width(width);
             warn(new_format, error);
             new_format
         })
index 2c1707b257a7c2b487174db4c196ee90d02d7801..c26009921bd88e7c1a37c826f2b51047b1496648 100644 (file)
@@ -12,7 +12,7 @@ use ordered_float::OrderedFloat;
 use unicase::UniCase;
 
 use crate::{
-    format::Spec,
+    format::Format,
     identifier::{ByIdentifier, HasIdentifier, Identifier},
     raw::{self, Alignment, CategoryLabels, Decoder, Measure, MissingValues, RawStr, VarType},
 };
@@ -343,8 +343,8 @@ pub struct Variable {
     pub name: Identifier,
     pub width: VarWidth,
     pub missing_values: MissingValues,
-    pub print_format: Spec,
-    pub write_format: Spec,
+    pub print_format: Format,
+    pub write_format: Format,
     pub value_labels: HashMap<Value, String>,
     pub label: Option<String>,
     pub measure: Option<Measure>,
@@ -364,8 +364,8 @@ impl Variable {
             name,
             width,
             missing_values: MissingValues::default(),
-            print_format: Spec::default_for_width(width),
-            write_format: Spec::default_for_width(width),
+            print_format: Format::default_for_width(width),
+            write_format: Format::default_for_width(width),
             value_labels: HashMap::new(),
             label: None,
             measure: Measure::default_for_type(var_type),
index 3692180dbaa098d847e005b9df3b5aa4de47d847..defd7f4bfa3919d257894976fdd3c5ea4c4aa1ee 100644 (file)
@@ -13,6 +13,11 @@ pub enum Endian {
 }
 
 impl Endian {
+    #[cfg(target_endian = "big")]
+    const NATIVE: Endian = Endian::Big;
+    #[cfg(target_endian = "little")]
+    const NATIVE: Endian = Endian::Little;
+
     pub fn identify_u32(expected_value: u32, bytes: [u8; 4]) -> Option<Self> {
         let as_big: u32 = Endian::Big.parse(bytes);
         let as_little: u32 = Endian::Little.parse(bytes);
index 0c5fca05bd02580bd4fb6a7715d6bd56026156bb..d0eba7d9b9c361f50ebb2bf611623909d10c00ce 100644 (file)
@@ -16,43 +16,43 @@ pub enum Error {
     #[error("Unknown format type {value}.")]
     UnknownFormat { value: u16 },
 
-    #[error("Output format {0} specifies width {}, but {} requires an even width.", .0.w, .0.format)]
-    OddWidthNotAllowed(UncheckedSpec),
+    #[error("Output format {0} specifies width {}, but {} requires an even width.", .0.w, .0.type_)]
+    OddWidthNotAllowed(UncheckedFormat),
 
-    #[error("Output format {0} specifies width {}, but {} requires a width between {} and {}.", .0.w, .0.format, .0.format.min_width(), .0.format.max_width())]
-    BadWidth(UncheckedSpec),
+    #[error("Output format {0} specifies width {}, but {} requires a width between {} and {}.", .0.w, .0.type_, .0.type_.min_width(), .0.type_.max_width())]
+    BadWidth(UncheckedFormat),
 
-    #[error("Output format {0} specifies decimal places, but {} format does not allow any decimals.", .0.format)]
-    DecimalsNotAllowedForFormat(UncheckedSpec),
+    #[error("Output format {0} specifies decimal places, but {} format does not allow any decimals.", .0.type_)]
+    DecimalsNotAllowedForFormat(UncheckedFormat),
 
-    #[error("Output format {0} specifies {} decimal places, but with a width of {}, {} does not allow any decimal places.", .0.d, .0.w, .0.format)]
-    DecimalsNotAllowedForWidth(UncheckedSpec),
+    #[error("Output format {0} specifies {} decimal places, but with a width of {}, {} does not allow any decimal places.", .0.d, .0.w, .0.type_)]
+    DecimalsNotAllowedForWidth(UncheckedFormat),
 
-    #[error("Output format {spec} specifies {} decimal places but, with a width of {}, {} allows at most {max_d} decimal places.", .spec.d, .spec.w, .spec.format)]
+    #[error("Output format {spec} specifies {} decimal places but, with a width of {}, {} allows at most {max_d} decimal places.", .spec.d, .spec.w, .spec.type_)]
     TooManyDecimalsForWidth {
-        spec: UncheckedSpec,
+        spec: UncheckedFormat,
         max_d: Decimals,
     },
 
     #[error("String variable is not compatible with numeric format {0}.")]
-    UnnamedVariableNotCompatibleWithNumericFormat(Format),
+    UnnamedVariableNotCompatibleWithNumericFormat(Type),
 
     #[error("Numeric variable is not compatible with string format {0}.")]
-    UnnamedVariableNotCompatibleWithStringFormat(Format),
+    UnnamedVariableNotCompatibleWithStringFormat(Type),
 
     #[error("String variable {variable} with width {width} is not compatible with format {bad_spec}.  Use format {good_spec} instead.")]
     NamedStringVariableBadSpecWidth {
         variable: String,
         width: Width,
-        bad_spec: Spec,
-        good_spec: Spec,
+        bad_spec: Format,
+        good_spec: Format,
     },
 
     #[error("String variable with width {width} is not compatible with format {bad_spec}.  Use format {good_spec} instead.")]
     UnnamedStringVariableBadSpecWidth {
         width: Width,
-        bad_spec: Spec,
-        good_spec: Spec,
+        bad_spec: Format,
+        good_spec: Format,
     },
 }
 
@@ -72,29 +72,27 @@ pub enum Category {
     String,
 }
 
-impl From<Format> for Category {
-    fn from(source: Format) -> Self {
+impl From<Type> for Category {
+    fn from(source: Type) -> Self {
         match source {
-            Format::F | Format::Comma | Format::Dot | Format::Dollar | Format::Pct | Format::E => {
-                Self::Basic
-            }
-            Format::CC(_) => Self::Custom,
-            Format::N | Format::Z => Self::Legacy,
-            Format::P | Format::PK | Format::IB | Format::PIB | Format::RB => Self::Binary,
-            Format::PIBHex | Format::RBHex => Self::Hex,
-            Format::Date
-            | Format::ADate
-            | Format::EDate
-            | Format::JDate
-            | Format::SDate
-            | Format::QYr
-            | Format::MoYr
-            | Format::WkYr
-            | Format::DateTime
-            | Format::YMDHMS => Self::Date,
-            Format::MTime | Format::Time | Format::DTime => Self::Time,
-            Format::WkDay | Format::Month => Self::DateComponent,
-            Format::A | Format::AHex => Self::String,
+            Type::F | Type::Comma | Type::Dot | Type::Dollar | Type::Pct | Type::E => Self::Basic,
+            Type::CC(_) => Self::Custom,
+            Type::N | Type::Z => Self::Legacy,
+            Type::P | Type::PK | Type::IB | Type::PIB | Type::RB => Self::Binary,
+            Type::PIBHex | Type::RBHex => Self::Hex,
+            Type::Date
+            | Type::ADate
+            | Type::EDate
+            | Type::JDate
+            | Type::SDate
+            | Type::QYr
+            | Type::MoYr
+            | Type::WkYr
+            | Type::DateTime
+            | Type::YMDHMS => Self::Date,
+            Type::MTime | Type::Time | Type::DTime => Self::Time,
+            Type::WkDay | Type::Month => Self::DateComponent,
+            Type::A | Type::AHex => Self::String,
         }
     }
 }
@@ -122,7 +120,7 @@ impl Display for CC {
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
-pub enum Format {
+pub enum Type {
     // Basic numeric formats.
     F,
     Comma,
@@ -176,7 +174,7 @@ pub type SignedWidth = i16;
 
 pub type Decimals = u8;
 
-impl Format {
+impl Type {
     pub fn max_width(self) -> Width {
         match self {
             Self::P | Self::PK | Self::PIBHex | Self::RBHex => 16,
@@ -321,7 +319,7 @@ impl Format {
     }
 }
 
-impl Display for Format {
+impl Display for Type {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
         let s = match self {
             Self::F => "F",
@@ -367,15 +365,21 @@ fn max_digits_for_bytes(bytes: usize) -> usize {
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
-pub struct Spec {
-    format: Format,
+pub struct Format {
+    type_: Type,
     w: Width,
     d: Decimals,
 }
 
-impl Spec {
-    pub fn format(self) -> Format {
-        self.format
+impl Format {
+    pub const F40: Format = Format {
+        type_: Type::F,
+        w: 40,
+        d: 0,
+    };
+
+    pub fn format(self) -> Type {
+        self.type_
     }
     pub fn w(self) -> Width {
         self.w
@@ -386,21 +390,25 @@ impl Spec {
 
     pub fn default_for_width(var_width: VarWidth) -> Self {
         match var_width {
-            VarWidth::Numeric => Spec {
-                format: Format::F,
+            VarWidth::Numeric => Format {
+                type_: Type::F,
                 w: 8,
                 d: 2,
             },
-            VarWidth::String(w) => Spec {
-                format: Format::A,
+            VarWidth::String(w) => Format {
+                type_: Type::A,
                 w,
                 d: 0,
             },
         }
     }
 
-    pub fn fixed_from(source: &UncheckedSpec) -> Self {
-        let UncheckedSpec { format, w, d } = *source;
+    pub fn fixed_from(source: &UncheckedFormat) -> Self {
+        let UncheckedFormat {
+            type_: format,
+            w,
+            d,
+        } = *source;
         let (min, max) = format.width_range().into_inner();
         let mut w = w.clamp(min, max);
         if d <= format.max_decimals(Width::MAX) {
@@ -410,34 +418,38 @@ impl Spec {
             }
         }
         let d = d.clamp(0, format.max_decimals(w));
-        Self { format, w, d }
+        Self {
+            type_: format,
+            w,
+            d,
+        }
     }
 
     pub fn var_width(self) -> VarWidth {
-        match self.format {
-            Format::A => VarWidth::String(self.w),
-            Format::AHex => VarWidth::String(self.w / 2),
+        match self.type_ {
+            Type::A => VarWidth::String(self.w),
+            Type::AHex => VarWidth::String(self.w / 2),
             _ => VarWidth::Numeric,
         }
     }
 
     pub fn var_type(self) -> VarType {
-        self.format.var_type()
+        self.type_.var_type()
     }
 
     /// Checks whether this format specification is valid for a variable with
     /// width `var_width`.
     pub fn check_width_compatibility(self, var_width: VarWidth) -> Result<Self, Error> {
         // Verify that the format is right for the variable's type.
-        self.format.check_type_compatibility(var_width.into())?;
+        self.type_.check_type_compatibility(var_width.into())?;
 
         if let VarWidth::String(w) = var_width {
             if var_width != self.var_width() {
                 let bad_spec = self;
-                let good_spec = if self.format == Format::A {
-                    Spec { w, ..self }
+                let good_spec = if self.type_ == Type::A {
+                    Format { w, ..self }
                 } else {
-                    Spec { w: w * 2, ..self }
+                    Format { w: w * 2, ..self }
                 };
                 return Err(Error::UnnamedStringVariableBadSpecWidth {
                     width: w,
@@ -451,21 +463,25 @@ impl Spec {
     }
 }
 
-impl Display for Spec {
+impl Display for Format {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        write!(f, "{}{}", self.format, self.w)?;
-        if self.format.takes_decimals() || self.d > 0 {
+        write!(f, "{}{}", self.type_, self.w)?;
+        if self.type_.takes_decimals() || self.d > 0 {
             write!(f, ".{}", self.d)?;
         }
         Ok(())
     }
 }
 
-impl TryFrom<UncheckedSpec> for Spec {
+impl TryFrom<UncheckedFormat> for Format {
     type Error = Error;
 
-    fn try_from(source: UncheckedSpec) -> Result<Self, Self::Error> {
-        let UncheckedSpec { format, w, d } = source;
+    fn try_from(source: UncheckedFormat) -> Result<Self, Self::Error> {
+        let UncheckedFormat {
+            type_: format,
+            w,
+            d,
+        } = source;
         let max_d = format.max_decimals(w);
         if w % format.width_step() != 0 {
             Err(Error::OddWidthNotAllowed(source))
@@ -483,12 +499,16 @@ impl TryFrom<UncheckedSpec> for Spec {
                 Err(Error::DecimalsNotAllowedForWidth(source))
             }
         } else {
-            Ok(Spec { format, w, d })
+            Ok(Format {
+                type_: format,
+                w,
+                d,
+            })
         }
     }
 }
 
-impl TryFrom<u16> for Format {
+impl TryFrom<u16> for Type {
     type Error = Error;
 
     fn try_from(source: u16) -> Result<Self, Self::Error> {
@@ -536,15 +556,15 @@ impl TryFrom<u16> for Format {
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
-pub struct UncheckedSpec {
-    pub format: Format,
+pub struct UncheckedFormat {
+    pub type_: Type,
 
     pub w: Width,
 
     pub d: Decimals,
 }
 
-impl TryFrom<raw::Spec> for UncheckedSpec {
+impl TryFrom<raw::Spec> for UncheckedFormat {
     type Error = Error;
 
     fn try_from(raw: raw::Spec) -> Result<Self, Self::Error> {
@@ -553,14 +573,18 @@ impl TryFrom<raw::Spec> for UncheckedSpec {
         let format = raw_format.try_into()?;
         let w = ((raw >> 8) & 0xff) as Width;
         let d = (raw & 0xff) as Decimals;
-        Ok(Self { format, w, d })
+        Ok(Self {
+            type_: format,
+            w,
+            d,
+        })
     }
 }
 
-impl Display for UncheckedSpec {
+impl Display for UncheckedFormat {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        write!(f, "{}{}", self.format, self.w)?;
-        if self.format.takes_decimals() || self.d > 0 {
+        write!(f, "{}{}", self.type_, self.w)?;
+        if self.type_.takes_decimals() || self.d > 0 {
             write!(f, ".{}", self.d)?;
         }
         Ok(())
@@ -570,7 +594,7 @@ impl Display for UncheckedSpec {
 pub struct Settings {
     epoch: Option<i32>,
 
-    /// Either `b'.'` or `b','`.
+    /// Either `'.'` or `','`.
     decimal: char,
 
     /// Format `F`, `E`, `COMMA`, and `DOT` with leading zero (e.g. `0.5`
@@ -578,7 +602,18 @@ pub struct Settings {
     include_leading_zero: bool,
 
     /// Custom currency styles.
-    ccs: EnumMap<CC, NumberStyle>,
+    ccs: EnumMap<CC, Option<NumberStyle>>,
+}
+
+impl Default for Settings {
+    fn default() -> Self {
+        Self {
+            epoch: None,
+            decimal: '.',
+            include_leading_zero: false,
+            ccs: Default::default(),
+        }
+    }
 }
 
 /// A numeric output style.  This can express numeric formats in
index b75411955f40af0460dbd754a1c0da48b6100ba5..4624edfe69b7780bd6634fd6342672fad3e4197f 100644 (file)
@@ -14,3 +14,4 @@ pub mod lex;
 pub mod prompt;
 pub mod message;
 pub mod macros;
+pub mod settings;
index f8682050c0f88b01d4bfe650398311e78a7bdccd..236592cad27ba84bbe8a6a386ec9a7472eb268f0 100644 (file)
@@ -5,6 +5,7 @@ use std::{
     sync::Arc,
 };
 
+use enum_map::Enum;
 use unicode_width::UnicodeWidthStr;
 
 /// A line number and optional column number within a source file.
@@ -127,3 +128,10 @@ impl Location {
         self.file_name.is_none() && self.span.is_none()
     }
 }
+
+#[derive(Enum)]
+pub enum Severity {
+    Error,
+    Warning,
+    Note,
+}
index fde78fbd81977eccfd37bbc214d3567ffe0a3472..d8f5c9f17f5a3c8eea8912ac261e527fd09b74fa 100644 (file)
 //!
 //! 5. Output the table for user consumption.  Use pivot_table_submit().
 
-use std::{collections::HashMap, ops::Range, sync::Arc};
+use std::{
+    collections::HashMap,
+    ops::Range,
+    sync::{Arc, OnceLock},
+};
 
 use chrono::NaiveDateTime;
-use enum_map::{Enum, EnumMap};
+use enum_map::{enum_map, Enum, EnumMap};
 
-use crate::format::{Settings as FormatSettings, Spec};
+use crate::format::{Format, Settings as FormatSettings};
 
 /// Areas of a pivot table for styling purposes.
 #[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)]
@@ -104,7 +108,7 @@ pub enum BoxBorder {
 }
 
 /// Borders between rows and columns.
-#[derive(Debug, Enum)]
+#[derive(Debug, Enum, PartialEq, Eq)]
 pub enum RowColBorder {
     RowHorz,
     RowVert,
@@ -116,6 +120,7 @@ pub enum RowColBorder {
 ///
 /// The comments below talk about columns and their widths but they apply
 /// equally to rows and their heights.
+#[derive(Default)]
 pub struct Sizing {
     /// Specific column widths, in 1/96" units.
     widths: Vec<i32>,
@@ -136,6 +141,7 @@ pub enum Axis3 {
 }
 
 /// An axis within a pivot table.
+#[derive(Default)]
 pub struct TableAxis {
     /// `dimensions[0]` is the innermost dimension.
     dimensions: Vec<Dimension>,
@@ -172,7 +178,7 @@ pub struct Dimension {
     ///
     /// The root must always be a group, although it is allowed to have no
     /// subcategories.
-    root: Arc<Category>,
+    root: Group,
 
     ///  All of the leaves reachable via the root.
     ///
@@ -185,8 +191,8 @@ pub struct Dimension {
     ///  `data_leaves[i]->data_index == i`.  This might differ from what an
     ///  in-order traversal of `root` would yield, if the user reordered
     ///  categories.
-    data_leaves: Vec<Arc<Category>>,
-    presentation_leaves: Vec<Arc<Category>>,
+    data_leaves: Vec<Arc<Leaf>>,
+    presentation_leaves: Vec<Arc<Leaf>>,
 
     /// Display.
     hide_all_labels: bool,
@@ -195,38 +201,100 @@ pub struct Dimension {
     label_depth: usize,
 }
 
-/// A pivot_category is a leaf (a category) or a group.
-pub struct Category {
+pub struct Group {
     name: Value,
     label_depth: usize,
     extra_depth: usize,
-    type_: CategoryType,
+
+    /// The child categories.
+    ///
+    /// A group usually has multiple children, but it is allowed to have
+    /// only one or even (pathologically) none.
+    children: Vec<Category>,
+
+    /// Display a label for the group itself?
+    show_label: bool,
+
+    show_label_in_corner: bool,
 }
 
-pub enum CategoryType {
-    Group {
-        /// The child categories.
-        ///
-        /// A group usually has multiple children, but it is allowed to have
-        /// only one or even (pathologically) none.
-        children: Vec<Box<Category>>,
+pub struct Leaf {
+    name: Value,
+    label_depth: usize,
+    extra_depth: usize,
+
+    group_index: usize,
+    data_index: usize,
+    presentation_index: usize,
 
-        /// Display a label for the group itself?
-        show_label: bool,
+    /// Default format for values in this category.
+    format: Format,
 
-        show_label_in_corner: bool,
-    },
-    Leaf {
-        group_index: usize,
-        data_index: usize,
-        presentation_index: usize,
+    /// Honor [Table]'s `small` setting?
+    honor_small: bool,
+}
 
-        /// Default format for values in this category.
-        format: Spec,
+/// A pivot_category is a leaf (a category) or a group.
+pub enum Category {
+    Group(Arc<Group>),
+    Leaf(Arc<Leaf>),
+}
 
-        /// Honor [Table]'s `small` setting?
-        honor_small: bool,
-    },
+trait CategoryTrait {
+    fn name(&self) -> &Value;
+    fn label_depth(&self) -> usize;
+    fn extra_depth(&self) -> usize;
+}
+
+impl CategoryTrait for Group {
+    fn name(&self) -> &Value {
+        &self.name
+    }
+
+    fn label_depth(&self) -> usize {
+        self.label_depth
+    }
+
+    fn extra_depth(&self) -> usize {
+        self.extra_depth
+    }
+}
+
+impl CategoryTrait for Leaf {
+    fn name(&self) -> &Value {
+        &self.name
+    }
+
+    fn label_depth(&self) -> usize {
+        self.label_depth
+    }
+
+    fn extra_depth(&self) -> usize {
+        self.extra_depth
+    }
+}
+
+impl CategoryTrait for Category {
+    fn name(&self) -> &Value {
+        match self {
+            Category::Group(group) => group.name(),
+            Category::Leaf(leaf) => leaf.name(),
+        }
+    }
+
+    fn label_depth(&self) -> usize {
+        match self {
+            Category::Group(group) => group.label_depth(),
+            Category::Leaf(leaf) => leaf.label_depth(),
+        }
+    }
+
+    fn extra_depth(&self) -> usize {
+        match self {
+            Category::Group(group) => group.extra_depth(),
+            Category::Leaf(leaf) => leaf.extra_depth(),
+        }
+    }
 }
 
 /// Styling for a pivot table.
@@ -275,6 +343,79 @@ struct Look {
     n_orphan_lines: usize,
 }
 
+impl Default for Look {
+    fn default() -> Self {
+        Self {
+            name: None,
+            omit_empty: true,
+            row_labels_in_corner: true,
+            row_heading_widths: 36..72,
+            col_heading_widths: 36..120,
+            footnote_marker_type: FootnoteMarkerType::Alphabetic,
+            footnote_marker_position: FootnoteMarkerPosition::Subscript,
+            areas: EnumMap::from_fn(|area| {
+                use HorzAlign::*;
+                use VertAlign::*;
+                let (halign, valign, hmargins, vmargins) = match area {
+                    Area::Title => (Center, Middle, [8, 11], [1, 8]),
+                    Area::Caption => (Left, Top, [8, 11], [1, 1]),
+                    Area::Footer => (Left, Top, [11, 8], [2, 3]),
+                    Area::Corner => (Left, Bottom, [8, 11], [1, 1]),
+                    Area::ColumnLabels => (Left, Top, [8, 11], [1, 3]),
+                    Area::RowLabels => (Left, Top, [8, 11], [1, 3]),
+                    Area::Data => (Mixed, Top, [8, 11], [1, 1]),
+                    Area::Layers => (Left, Bottom, [8, 11], [1, 3]),
+                };
+                AreaStyle {
+                    cell_style: CellStyle {
+                        horz_align: halign,
+                        vert_align: valign,
+                        margins: enum_map! { Axis2::X => hmargins, Axis2::Y => vmargins },
+                    },
+                    font_style: FontStyle {
+                        bold: area == Area::Title,
+                        italic: false,
+                        underline: false,
+                        markup: false,
+                        font: String::from("Sans Serif"),
+                        fg: [Color::BLACK; 2],
+                        bg: [Color::WHITE; 2],
+                        size: 9,
+                    },
+                }
+            }),
+            borders: EnumMap::from_fn(|border| {
+                let stroke = match border {
+                    Border::InnerFrame(_) | Border::DataLeft | Border::DataTop => Stroke::Thick,
+                    Border::Dimensions(side) if side != RowColBorder::RowVert => Stroke::Solid,
+                    Border::Categories(RowColBorder::ColHorz | RowColBorder::ColVert) => {
+                        Stroke::Solid
+                    }
+                    _ => Stroke::None,
+                };
+                BorderStyle {
+                    stroke,
+                    color: Color::BLACK,
+                }
+            }),
+            print_all_layers: false,
+            paginate_layers: false,
+            shrink_to_fit: EnumMap::from_fn(|_| false),
+            top_continuation: false,
+            bottom_continuation: false,
+            continuation: None,
+            n_orphan_lines: 0,
+        }
+    }
+}
+
+impl Look {
+    fn shared_default() -> Arc<Look> {
+        static LOOK: OnceLock<Arc<Look>> = OnceLock::new();
+        LOOK.get_or_init(|| Arc::new(Look::default())).clone()
+    }
+}
+
 pub struct AreaStyle {
     cell_style: CellStyle,
     font_style: FontStyle,
@@ -321,7 +462,7 @@ pub enum VertAlign {
     Top,
 
     /// Centered,
-    Center,
+    Middle,
 
     /// Bottom alignment.
     Bottom,
@@ -347,6 +488,20 @@ pub struct Color {
     b: u8,
 }
 
+impl Color {
+    const BLACK: Color = Color::new(0, 0, 0);
+    const WHITE: Color = Color::new(255, 255, 255);
+
+    const fn new(r: u8, g: u8, b: u8) -> Self {
+        Self {
+            alpha: 255,
+            r,
+            g,
+            b,
+        }
+    }
+}
+
 pub struct BorderStyle {
     stroke: Stroke,
     color: Color,
@@ -401,7 +556,7 @@ pub struct Table {
 
     show_variables: Option<ValueShow>,
 
-    weight_format: Spec,
+    weight_format: Format,
 
     /// Current layer indexes, with axes[PIVOT_AXIS_LAYER].n_dimensions
     /// elements.  current_layer[i] is an offset into
@@ -410,17 +565,14 @@ pub struct Table {
     /// and there's no corresponding leaf.
     current_layer: Vec<usize>,
 
-    /// Column sizing and page breaks.
-    column_sizing: Sizing,
-
-    /// Row sizing and page breaks.
-    row_sizing: Sizing,
+    /// Column and row sizing and page breaks.
+    sizing: EnumMap<Axis2, Sizing>,
 
     /// Format settings.
     settings: FormatSettings,
 
     /// Numeric grouping character (usually `.` or `,`).
-    grouping: char,
+    grouping: Option<char>,
 
     small: f64,
 
@@ -432,16 +584,53 @@ pub struct Table {
     datafile: Option<String>,
     date: Option<NaiveDateTime>,
     footnotes: Vec<Footnote>,
-    title: Value,
-    subtype: Value,
-    corner_text: Value,
-    caption: Value,
+    title: Option<Value>,
+    subtype: Option<Value>,
+    corner_text: Option<Value>,
+    caption: Option<Value>,
     notes: Option<String>,
     dimensions: Vec<Dimension>,
     axes: EnumMap<Axis3, TableAxis>,
     cells: HashMap<u64, Value>,
 }
 
+impl Table {
+    fn new() -> Self {
+        Self {
+            look: Look::shared_default(),
+            rotate_inner_column_labels: false,
+            rotate_outer_row_labels: false,
+            show_grid_lines: false,
+            show_title: true,
+            show_caption: true,
+            show_value: None,
+            show_variables: None,
+            weight_format: Format::F40,
+            current_layer: Vec::new(),
+            sizing: EnumMap::default(),
+            settings: FormatSettings::default(), // XXX from settings
+            grouping: None,
+            small: 0.0001, // XXX from settings.
+            command_local: None,
+            command_c: None, // XXX from current command name.
+            language: None,
+            locale: None,
+            dataset: None,
+            datafile: None,
+            date: None,
+            footnotes: Vec::new(),
+            subtype: None,
+            title: None,
+            corner_text: None,
+            caption: None,
+            notes: None,
+            dimensions: Vec::new(),
+            axes: EnumMap::default(),
+            cells: HashMap::new(),
+        }
+    }
+}
+
 /// Whether to show variable or value labels or the underlying value or variable name.
 pub enum ValueShow {
     /// Value or variable name only.
@@ -507,7 +696,7 @@ pub struct Value {
 pub enum ValueInner {
     Number {
         show: ValueShow,
-        format: Spec,
+        format: Format,
         honor_small: bool,
         value: f64,
         var_name: Option<String>,
diff --git a/rust/src/settings.rs b/rust/src/settings.rs
new file mode 100644 (file)
index 0000000..65b0826
--- /dev/null
@@ -0,0 +1,89 @@
+use enum_map::EnumMap;
+
+use crate::{endian::Endian, format::Format, message::Severity, format::Settings as FormatSettings};
+
+pub struct Settings {
+    input_integer_format: Endian,
+    input_float_format: Endian,
+    output_integer_format: Endian,
+    output_float_format: Endian,
+
+    /// `MDISPLAY`: how to display matrices in `MATRIX`...`END MATRIX`.
+    matrix_display: MatrixDisplay,
+
+    view_length: usize,
+
+    view_width: usize,
+
+    safer: bool,
+
+    include: bool,
+
+    route_errors_to_terminal: bool,
+
+    route_errors_to_listing: bool,
+
+    scompress: bool,
+
+    undefined: bool,
+
+    blanks: f64,
+
+    max_messages: EnumMap<Severity, usize>,
+    printback: bool,
+    macros: MacroSettings,
+    max_loops: usize,
+    workspace: usize,
+    default_format: Format,
+    testing: bool,
+    fuzz_bits: usize,
+    scale_min: usize,
+    commands: Compatibility,
+    global: Compatibility,
+    syntax: Compatibility,
+    formats: FormatSettings,
+    small: f64,
+    
+}
+
+pub enum Compatibility {
+    Compatible,
+    Enhanced,
+}
+
+pub struct MacroSettings {
+    /// Expand macros?
+    expand: bool,
+
+    /// Print macro expansions?
+    print_expansions: bool,
+
+    /// Maximum iterations of `!FOR`.
+    max_iterations: usize,
+
+    /// Maximum nested macro expansion levels.
+    max_nest: usize,
+}
+
+/// How to display matrices in `MATRIX`...`END MATRIX`.
+pub enum MatrixDisplay {
+    /// Output matrices as text.
+    Text,
+
+    /// Output matrices as pivot tables.
+    Tables,
+}
+
+pub enum OutputType {
+    /// Errors and warnings.
+    Error,
+
+    /// Notes.
+    Notes,
+
+    /// Syntax printback.
+    Syntax,
+
+    /// Everything else.
+    Other,
+}