more cleanup
authorBen Pfaff <blp@cs.stanford.edu>
Sat, 12 Jul 2025 23:43:21 +0000 (16:43 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sat, 12 Jul 2025 23:43:21 +0000 (16:43 -0700)
rust/pspp/src/dictionary.rs
rust/pspp/src/sys/cooked.rs
rust/pspp/src/sys/raw.rs

index 854c6dd66d19fada53c49b9ac24c42de1cc61e29..c555b30abff17cff4d61cfa51a4ebd0e2dc050fe 100644 (file)
@@ -36,11 +36,11 @@ use thiserror::Error as ThisError;
 use unicase::UniCase;
 
 use crate::{
-    format::Format,
+    format::{DisplayPlain, Format},
     identifier::{ByIdentifier, HasIdentifier, Identifier},
     output::pivot::{Axis3, Dimension, Footnote, Footnotes, Group, PivotTable, Value},
     settings::Show,
-    sys::raw::{Alignment, CategoryLabels, Measure, MissingValues, RawString, VarType},
+    sys::raw::{CategoryLabels, RawString, VarType},
 };
 
 /// An index within [Dictionary::variables].
@@ -1406,6 +1406,262 @@ impl ValueLabels {
     }
 }
 
+#[derive(Clone, Default)]
+pub struct MissingValues {
+    /// Individual missing values, up to 3 of them.
+    values: Vec<Datum>,
+
+    /// Optional range of missing values.
+    range: Option<MissingValueRange>,
+}
+
+impl Debug for MissingValues {
+    fn fmt(&self, f: &mut Formatter) -> FmtResult {
+        DisplayMissingValues {
+            mv: self,
+            encoding: None,
+        }
+        .fmt(f)
+    }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum MissingValuesError {
+    TooMany,
+    TooWide,
+    MixedTypes,
+}
+
+impl MissingValues {
+    pub fn new(
+        mut values: Vec<Datum>,
+        range: Option<MissingValueRange>,
+    ) -> Result<Self, MissingValuesError> {
+        if values.len() > 3 {
+            return Err(MissingValuesError::TooMany);
+        }
+
+        let mut var_type = None;
+        for value in values.iter_mut() {
+            value.trim_end();
+            match value.width() {
+                VarWidth::String(w) if w > 8 => return Err(MissingValuesError::TooWide),
+                _ => (),
+            }
+            if var_type.is_some_and(|t| t != value.var_type()) {
+                return Err(MissingValuesError::MixedTypes);
+            }
+            var_type = Some(value.var_type());
+        }
+
+        if var_type == Some(VarType::String) && range.is_some() {
+            return Err(MissingValuesError::MixedTypes);
+        }
+
+        Ok(Self { values, range })
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.values.is_empty() && self.range.is_none()
+    }
+
+    pub fn var_type(&self) -> Option<VarType> {
+        if let Some(datum) = self.values.first() {
+            Some(datum.var_type())
+        } else if self.range.is_some() {
+            Some(VarType::Numeric)
+        } else {
+            None
+        }
+    }
+
+    pub fn contains(&self, value: &Datum) -> bool {
+        if self
+            .values
+            .iter()
+            .any(|datum| datum.eq_ignore_trailing_spaces(value))
+        {
+            return true;
+        }
+
+        match value {
+            Datum::Number(Some(number)) => self.range.is_some_and(|range| range.contains(*number)),
+            _ => false,
+        }
+    }
+
+    pub fn is_resizable(&self, width: VarWidth) -> bool {
+        self.values.iter().all(|datum| datum.is_resizable(width))
+            && self.range.iter().all(|range| range.is_resizable(width))
+    }
+
+    pub fn resize(&mut self, width: VarWidth) {
+        for datum in &mut self.values {
+            datum.resize(width);
+        }
+        if let Some(range) = &mut self.range {
+            range.resize(width);
+        }
+    }
+
+    pub fn display(&self, encoding: &'static Encoding) -> DisplayMissingValues<'_> {
+        DisplayMissingValues {
+            mv: self,
+            encoding: Some(encoding),
+        }
+    }
+}
+
+pub struct DisplayMissingValues<'a> {
+    mv: &'a MissingValues,
+    encoding: Option<&'static Encoding>,
+}
+
+impl<'a> Display for DisplayMissingValues<'a> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        if let Some(range) = &self.mv.range {
+            write!(f, "{range}")?;
+            if !self.mv.values.is_empty() {
+                write!(f, "; ")?;
+            }
+        }
+
+        for (i, value) in self.mv.values.iter().enumerate() {
+            if i > 0 {
+                write!(f, "; ")?;
+            }
+            match self.encoding {
+                Some(encoding) => value.display_plain(encoding).fmt(f)?,
+                None => value.fmt(f)?,
+            }
+        }
+
+        if self.mv.is_empty() {
+            write!(f, "none")?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Copy, Clone)]
+pub enum MissingValueRange {
+    In { low: f64, high: f64 },
+    From { low: f64 },
+    To { high: f64 },
+}
+
+impl MissingValueRange {
+    pub fn new(low: f64, high: f64) -> Self {
+        const LOWEST: f64 = f64::MIN.next_up();
+        match (low, high) {
+            (f64::MIN | LOWEST, _) => Self::To { high },
+            (_, f64::MAX) => Self::From { low },
+            (_, _) => Self::In { low, high },
+        }
+    }
+
+    pub fn low(&self) -> Option<f64> {
+        match self {
+            MissingValueRange::In { low, .. } | MissingValueRange::From { low } => Some(*low),
+            MissingValueRange::To { .. } => None,
+        }
+    }
+
+    pub fn high(&self) -> Option<f64> {
+        match self {
+            MissingValueRange::In { high, .. } | MissingValueRange::To { high } => Some(*high),
+            MissingValueRange::From { .. } => None,
+        }
+    }
+
+    pub fn contains(&self, number: f64) -> bool {
+        match self {
+            MissingValueRange::In { low, high } => (*low..*high).contains(&number),
+            MissingValueRange::From { low } => number >= *low,
+            MissingValueRange::To { high } => number <= *high,
+        }
+    }
+
+    pub fn is_resizable(&self, width: VarWidth) -> bool {
+        width.is_numeric()
+    }
+
+    pub fn resize(&self, width: VarWidth) {
+        assert_eq!(width, VarWidth::Numeric);
+    }
+}
+
+impl Display for MissingValueRange {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        match self.low() {
+            Some(low) => low.display_plain().fmt(f)?,
+            None => write!(f, "LOW")?,
+        }
+
+        write!(f, " THRU ")?;
+
+        match self.high() {
+            Some(high) => high.display_plain().fmt(f)?,
+            None => write!(f, "HIGH")?,
+        }
+        Ok(())
+    }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Alignment {
+    Left,
+    Right,
+    Center,
+}
+
+impl Alignment {
+    pub fn default_for_type(var_type: VarType) -> Self {
+        match var_type {
+            VarType::Numeric => Self::Right,
+            VarType::String => Self::Left,
+        }
+    }
+
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            Alignment::Left => "Left",
+            Alignment::Right => "Right",
+            Alignment::Center => "Center",
+        }
+    }
+}
+
+/// [Level of measurement](https://en.wikipedia.org/wiki/Level_of_measurement).
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Measure {
+    /// Nominal values can only be compared for equality.
+    Nominal,
+
+    /// Ordinal values can be meaningfully ordered.
+    Ordinal,
+
+    /// Scale values can be meaningfully compared for the degree of difference.
+    Scale,
+}
+
+impl Measure {
+    pub fn default_for_type(var_type: VarType) -> Option<Measure> {
+        match var_type {
+            VarType::Numeric => None,
+            VarType::String => Some(Self::Nominal),
+        }
+    }
+
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            Measure::Nominal => "Nominal",
+            Measure::Ordinal => "Ordinal",
+            Measure::Scale => "Scale",
+        }
+    }
+}
+
 #[cfg(test)]
 mod test {
     use std::collections::HashSet;
index 5a68a2d43d6cb56b9024b5f17fec2b1fd67618e6..3b50a63ce5aed6ed2ac104859c6fe1e927ed7627 100644 (file)
@@ -19,8 +19,7 @@ use std::{collections::BTreeMap, ops::Range};
 use crate::{
     calendar::date_time_to_pspp,
     dictionary::{
-        Datum, Dictionary, InvalidRole, MultipleResponseSet, MultipleResponseType, VarWidth,
-        Variable, VariableSet,
+        Datum, Dictionary, InvalidRole, MissingValues, MissingValuesError, MultipleResponseSet, MultipleResponseType, VarWidth, Variable, VariableSet
     },
     endian::Endian,
     format::{Error as FormatError, Format, UncheckedFormat},
@@ -33,10 +32,9 @@ use crate::{
             self, Cases, DecodedRecord, DocumentRecord, EncodingRecord, Extension,
             FileAttributesRecord, FloatInfoRecord, HeaderRecord, IntegerInfoRecord, LongName,
             LongNamesRecord, LongStringMissingValueRecord, LongStringValueLabelRecord,
-            MissingValues, MissingValuesError, MultipleResponseRecord, NumberOfCasesRecord,
-            ProductInfoRecord, RawDatum, RawString, RawWidth, ValueLabel,
-            ValueLabelRecord, VarDisplayRecord, VariableAttributesRecord, VariableRecord,
-            VariableSetRecord, VeryLongStringsRecord, ZHeader, ZTrailer,
+            MultipleResponseRecord, NumberOfCasesRecord, ProductInfoRecord, RawDatum, RawString,
+            RawWidth, ValueLabel, ValueLabelRecord, VarDisplayRecord, VariableAttributesRecord,
+            VariableRecord, VariableSetRecord, VeryLongStringsRecord, ZHeader, ZTrailer,
         },
     },
 };
index fb65892999c5e42d383ed847e21965a2578321ce..a1da804a6f70597249eaa3ea07f0e222d4873b1b 100644 (file)
 //! raw details.  Most readers will want to use higher-level interfaces.
 
 use crate::{
-    dictionary::{Attributes, Datum, VarWidth},
+    dictionary::{
+        Alignment, Attributes, Datum, Measure, MissingValueRange, MissingValues, VarWidth,
+    },
     endian::{Endian, Parse, ToBytes},
-    format::{DisplayPlain, DisplayPlainF64},
+    format::DisplayPlainF64,
     identifier::{Error as IdError, Identifier},
     sys::encoding::{default_encoding, get_encoding, Error as EncodingError},
 };
@@ -66,16 +68,22 @@ pub enum Error {
     #[error("Invalid ZSAV compression code {0}")]
     InvalidZsavCompression(u32),
 
-    #[error("Document record at offset {offset:#x} has document line count ({n}) greater than the maximum number {max}.")]
+    #[error(
+        "Document record at offset {offset:#x} has document line count ({n}) greater than the maximum number {max}."
+    )]
     BadDocumentLength { offset: u64, n: usize, max: usize },
 
     #[error("At offset {offset:#x}, unrecognized record type {rec_type}.")]
     BadRecordType { offset: u64, rec_type: u32 },
 
-    #[error("In variable record starting at offset {start_offset:#x}, variable width is not in the valid range -1 to 255.")]
+    #[error(
+        "In variable record starting at offset {start_offset:#x}, variable width is not in the valid range -1 to 255."
+    )]
     BadVariableWidth { start_offset: u64, width: i32 },
 
-    #[error("In variable record starting at offset {start_offset:#x}, variable label code {code} at offset {code_offset:#x} is not 0 or 1.")]
+    #[error(
+        "In variable record starting at offset {start_offset:#x}, variable label code {code} at offset {code_offset:#x} is not 0 or 1."
+    )]
     BadVariableLabelCode {
         start_offset: u64,
         code_offset: u64,
@@ -93,16 +101,24 @@ pub enum Error {
     #[error("At offset {offset:#x}, string missing value code ({code}) is not 0, 1, 2, or 3.")]
     BadStringMissingValueCode { offset: u64, code: i32 },
 
-    #[error("At offset {offset:#x}, number of value labels ({n}) is greater than the maximum number {max}.")]
+    #[error(
+        "At offset {offset:#x}, number of value labels ({n}) is greater than the maximum number {max}."
+    )]
     BadNumberOfValueLabels { offset: u64, n: u32, max: u32 },
 
-    #[error("At offset {offset:#x}, following value label record, found record type {rec_type} instead of expected type 4 for variable index record")]
+    #[error(
+        "At offset {offset:#x}, following value label record, found record type {rec_type} instead of expected type 4 for variable index record"
+    )]
     ExpectedVarIndexRecord { offset: u64, rec_type: u32 },
 
-    #[error("At offset {offset:#x}, number of variables indexes for value labels ({n}) is greater than the maximum number ({max}).")]
+    #[error(
+        "At offset {offset:#x}, number of variables indexes for value labels ({n}) is greater than the maximum number ({max})."
+    )]
     TooManyVarIndexes { offset: u64, n: u32, max: u32 },
 
-    #[error("At offset {offset:#x}, record type 7 subtype {subtype} is too large with element size {size} and {count} elements.")]
+    #[error(
+        "At offset {offset:#x}, record type 7 subtype {subtype} is too large with element size {size} and {count} elements."
+    )]
     ExtensionRecordTooLarge {
         offset: u64,
         subtype: u32,
@@ -110,7 +126,9 @@ pub enum Error {
         count: u32,
     },
 
-    #[error("Unexpected end of file at offset {offset:#x}, {case_ofs} bytes into a {case_len}-byte case.")]
+    #[error(
+        "Unexpected end of file at offset {offset:#x}, {case_ofs} bytes into a {case_len}-byte case."
+    )]
     EofInCase {
         offset: u64,
         case_ofs: u64,
@@ -129,10 +147,14 @@ pub enum Error {
     #[error("Data ends at offset {offset:#x}, {case_ofs} bytes into a compressed case.")]
     PartialCompressedCase { offset: u64, case_ofs: u64 },
 
-    #[error("At {case_ofs} bytes into compressed case starting at offset {offset:#x}, a string was found where a number was expected.")]
+    #[error(
+        "At {case_ofs} bytes into compressed case starting at offset {offset:#x}, a string was found where a number was expected."
+    )]
     CompressedNumberExpected { offset: u64, case_ofs: u64 },
 
-    #[error("At {case_ofs} bytes into compressed case starting at offset {offset:#x}, a number was found where a string was expected.")]
+    #[error(
+        "At {case_ofs} bytes into compressed case starting at offset {offset:#x}, a number was found where a string was expected."
+    )]
     CompressedStringExpected { offset: u64, case_ofs: u64 },
 
     #[error("Impossible ztrailer_offset {0:#x}.")]
@@ -156,7 +178,9 @@ pub enum Error {
     #[error("ZLIB trailer specifies unexpected {0}-byte block size.")]
     WrongZlibTrailerBlockSize(u32),
 
-    #[error("Block count {n_blocks} in ZLIB trailer at offset {offset:#x} differs from expected block count {expected_n_blocks} calculated from trailer length {ztrailer_len}.")]
+    #[error(
+        "Block count {n_blocks} in ZLIB trailer at offset {offset:#x} differs from expected block count {expected_n_blocks} calculated from trailer length {ztrailer_len}."
+    )]
     BadZlibTrailerNBlocks {
         offset: u64,
         n_blocks: u32,
@@ -164,28 +188,36 @@ pub enum Error {
         ztrailer_len: u64,
     },
 
-    #[error("ZLIB block descriptor {index} reported uncompressed data offset {actual:#x}, when {expected:#x} was expected.")]
+    #[error(
+        "ZLIB block descriptor {index} reported uncompressed data offset {actual:#x}, when {expected:#x} was expected."
+    )]
     ZlibTrailerBlockWrongUncmpOfs {
         index: usize,
         actual: u64,
         expected: u64,
     },
 
-    #[error("ZLIB block descriptor {index} reported compressed data offset {actual:#x}, when {expected:#x} was expected.")]
+    #[error(
+        "ZLIB block descriptor {index} reported compressed data offset {actual:#x}, when {expected:#x} was expected."
+    )]
     ZlibTrailerBlockWrongCmpOfs {
         index: usize,
         actual: u64,
         expected: u64,
     },
 
-    #[error("ZLIB block descriptor {index} reports compressed size {compressed_size} and uncompressed size {uncompressed_size}.")]
+    #[error(
+        "ZLIB block descriptor {index} reports compressed size {compressed_size} and uncompressed size {uncompressed_size}."
+    )]
     ZlibExpansion {
         index: usize,
         compressed_size: u32,
         uncompressed_size: u32,
     },
 
-    #[error("ZLIB trailer is at offset {zheader:#x} but {descriptors:#x} would be expected from block descriptors.")]
+    #[error(
+        "ZLIB trailer is at offset {zheader:#x} but {descriptors:#x} would be expected from block descriptors."
+    )]
     ZlibTrailerOffsetInconsistency { descriptors: u64, zheader: u64 },
 
     #[error("File metadata says it contains {expected} cases, but {actual} cases were read.")]
@@ -204,7 +236,9 @@ pub enum Warning {
     #[error("Unexpected end of data inside extension record.")]
     UnexpectedEndOfData,
 
-    #[error("At offset {offset:#x}, at least one valid variable index for value labels is required but none were specified.")]
+    #[error(
+        "At offset {offset:#x}, at least one valid variable index for value labels is required but none were specified."
+    )]
     NoVarIndexes { offset: u64 },
 
     #[error("At offset {offset:#x}, the first variable index is for a {var_type} variable but the following variable indexes are for {} variables: {wrong_types:?}", !var_type)]
@@ -214,14 +248,18 @@ pub enum Warning {
         wrong_types: Vec<u32>,
     },
 
-    #[error("At offset {offset:#x}, one or more variable indexes for value labels were not in the valid range [1,{max}] or referred to string continuations: {invalid:?}")]
+    #[error(
+        "At offset {offset:#x}, one or more variable indexes for value labels were not in the valid range [1,{max}] or referred to string continuations: {invalid:?}"
+    )]
     InvalidVarIndexes {
         offset: u64,
         max: usize,
         invalid: Vec<u32>,
     },
 
-    #[error("At offset {offset:#x}, {record} has bad size {size} bytes instead of the expected {expected_size}.")]
+    #[error(
+        "At offset {offset:#x}, {record} has bad size {size} bytes instead of the expected {expected_size}."
+    )]
     BadRecordSize {
         offset: u64,
         record: String,
@@ -229,7 +267,9 @@ pub enum Warning {
         expected_size: u32,
     },
 
-    #[error("At offset {offset:#x}, {record} has bad count {count} instead of the expected {expected_count}.")]
+    #[error(
+        "At offset {offset:#x}, {record} has bad count {count} instead of the expected {expected_count}."
+    )]
     BadRecordCount {
         offset: u64,
         record: String,
@@ -237,14 +277,18 @@ pub enum Warning {
         expected_count: u32,
     },
 
-    #[error("In long string missing values record starting at offset {record_offset:#x}, value length at offset {offset:#x} is {value_len} instead of the expected 8.")]
+    #[error(
+        "In long string missing values record starting at offset {record_offset:#x}, value length at offset {offset:#x} is {value_len} instead of the expected 8."
+    )]
     BadLongMissingValueLength {
         record_offset: u64,
         offset: u64,
         value_len: u32,
     },
 
-    #[error("The encoding record at offset {offset:#x} contains an encoding name that is not valid UTF-8.")]
+    #[error(
+        "The encoding record at offset {offset:#x} contains an encoding name that is not valid UTF-8."
+    )]
     BadEncodingName { offset: u64 },
 
     // XXX This is risky because `text` might be arbitarily long.
@@ -323,7 +367,9 @@ pub enum Warning {
     #[error("Syntax error parsing counted string (length {0:?} goes past end of input)")]
     CountedStringTooLong(usize),
 
-    #[error("Variable display record contains {count} items but should contain either {first} or {second}.")]
+    #[error(
+        "Variable display record contains {count} items but should contain either {first} or {second}."
+    )]
     InvalidVariableDisplayCount {
         count: usize,
         first: usize,
@@ -357,14 +403,18 @@ pub enum Warning {
     #[error("Compression bias is {0} instead of the usual values of 0 or 100.")]
     UnexpectedBias(f64),
 
-    #[error("ZLIB block descriptor {index} reported block size {actual:#x}, when {expected:#x} was expected.")]
+    #[error(
+        "ZLIB block descriptor {index} reported block size {actual:#x}, when {expected:#x} was expected."
+    )]
     ZlibTrailerBlockWrongSize {
         index: usize,
         actual: u32,
         expected: u32,
     },
 
-    #[error("ZLIB block descriptor {index} reported block size {actual:#x}, when at most {max_expected:#x} was expected.")]
+    #[error(
+        "ZLIB block descriptor {index} reported block size {actual:#x}, when at most {max_expected:#x} was expected."
+    )]
     ZlibTrailerBlockTooBig {
         index: usize,
         actual: u32,
@@ -1673,104 +1723,7 @@ fn format_name(type_: u32) -> Cow<'static, str> {
     .into()
 }
 
-#[derive(Clone, Default)]
-pub struct MissingValues {
-    /// Individual missing values, up to 3 of them.
-    values: Vec<Datum>,
-
-    /// Optional range of missing values.
-    range: Option<MissingValueRange>,
-}
-
-impl Debug for MissingValues {
-    fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        DisplayMissingValues {
-            mv: self,
-            encoding: None,
-        }
-        .fmt(f)
-    }
-}
-
-#[derive(Copy, Clone, Debug)]
-pub enum MissingValuesError {
-    TooMany,
-    TooWide,
-    MixedTypes,
-}
-
 impl MissingValues {
-    pub fn new(
-        mut values: Vec<Datum>,
-        range: Option<MissingValueRange>,
-    ) -> Result<Self, MissingValuesError> {
-        if values.len() > 3 {
-            return Err(MissingValuesError::TooMany);
-        }
-
-        let mut var_type = None;
-        for value in values.iter_mut() {
-            value.trim_end();
-            match value.width() {
-                VarWidth::String(w) if w > 8 => return Err(MissingValuesError::TooWide),
-                _ => (),
-            }
-            if var_type.is_some_and(|t| t != value.var_type()) {
-                return Err(MissingValuesError::MixedTypes);
-            }
-            var_type = Some(value.var_type());
-        }
-
-        if var_type == Some(VarType::String) && range.is_some() {
-            return Err(MissingValuesError::MixedTypes);
-        }
-
-        Ok(Self { values, range })
-    }
-
-    pub fn is_empty(&self) -> bool {
-        self.values.is_empty() && self.range.is_none()
-    }
-
-    pub fn var_type(&self) -> Option<VarType> {
-        if let Some(datum) = self.values.first() {
-            Some(datum.var_type())
-        } else if self.range.is_some() {
-            Some(VarType::Numeric)
-        } else {
-            None
-        }
-    }
-
-    pub fn contains(&self, value: &Datum) -> bool {
-        if self
-            .values
-            .iter()
-            .any(|datum| datum.eq_ignore_trailing_spaces(value))
-        {
-            return true;
-        }
-
-        match value {
-            Datum::Number(Some(number)) => self.range.is_some_and(|range| range.contains(*number)),
-            _ => false,
-        }
-    }
-
-    pub fn is_resizable(&self, width: VarWidth) -> bool {
-        self.values.iter().all(|datum| datum.is_resizable(width))
-            && self.range.iter().all(|range| range.is_resizable(width))
-    }
-
-    pub fn resize(&mut self, width: VarWidth) {
-        for datum in &mut self.values {
-            datum.resize(width);
-        }
-        if let Some(range) = &mut self.range {
-            range.resize(width);
-        }
-    }
-
     fn read<R: Read + Seek>(
         r: &mut R,
         offset: u64,
@@ -1824,109 +1777,6 @@ impl MissingValues {
         }
         Ok(Self::default())
     }
-
-    pub fn display(&self, encoding: &'static Encoding) -> DisplayMissingValues<'_> {
-        DisplayMissingValues {
-            mv: self,
-            encoding: Some(encoding),
-        }
-    }
-}
-
-pub struct DisplayMissingValues<'a> {
-    mv: &'a MissingValues,
-    encoding: Option<&'static Encoding>,
-}
-
-impl<'a> Display for DisplayMissingValues<'a> {
-    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
-        if let Some(range) = &self.mv.range {
-            write!(f, "{range}")?;
-            if !self.mv.values.is_empty() {
-                write!(f, "; ")?;
-            }
-        }
-
-        for (i, value) in self.mv.values.iter().enumerate() {
-            if i > 0 {
-                write!(f, "; ")?;
-            }
-            match self.encoding {
-                Some(encoding) => value.display_plain(encoding).fmt(f)?,
-                None => value.fmt(f)?,
-            }
-        }
-
-        if self.mv.is_empty() {
-            write!(f, "none")?;
-        }
-        Ok(())
-    }
-}
-
-#[derive(Copy, Clone)]
-pub enum MissingValueRange {
-    In { low: f64, high: f64 },
-    From { low: f64 },
-    To { high: f64 },
-}
-
-impl MissingValueRange {
-    pub fn new(low: f64, high: f64) -> Self {
-        const LOWEST: f64 = f64::MIN.next_up();
-        match (low, high) {
-            (f64::MIN | LOWEST, _) => Self::To { high },
-            (_, f64::MAX) => Self::From { low },
-            (_, _) => Self::In { low, high },
-        }
-    }
-
-    pub fn low(&self) -> Option<f64> {
-        match self {
-            MissingValueRange::In { low, .. } | MissingValueRange::From { low } => Some(*low),
-            MissingValueRange::To { .. } => None,
-        }
-    }
-
-    pub fn high(&self) -> Option<f64> {
-        match self {
-            MissingValueRange::In { high, .. } | MissingValueRange::To { high } => Some(*high),
-            MissingValueRange::From { .. } => None,
-        }
-    }
-
-    pub fn contains(&self, number: f64) -> bool {
-        match self {
-            MissingValueRange::In { low, high } => (*low..*high).contains(&number),
-            MissingValueRange::From { low } => number >= *low,
-            MissingValueRange::To { high } => number <= *high,
-        }
-    }
-
-    pub fn is_resizable(&self, width: VarWidth) -> bool {
-        width.is_numeric()
-    }
-
-    pub fn resize(&self, width: VarWidth) {
-        assert_eq!(width, VarWidth::Numeric);
-    }
-}
-
-impl Display for MissingValueRange {
-    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
-        match self.low() {
-            Some(low) => low.display_plain().fmt(f)?,
-            None => write!(f, "LOW")?,
-        }
-
-        write!(f, " THRU ")?;
-
-        match self.high() {
-            Some(high) => high.display_plain().fmt(f)?,
-            None => write!(f, "HIGH")?,
-        }
-        Ok(())
-    }
 }
 
 #[derive(Clone)]
@@ -1956,19 +1806,30 @@ where
     pub label: Option<S>,
 }
 
+/// Width of a variable record.
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum RawWidth {
+    /// String continuation.
+    ///
+    /// One variable record of this type is present for each 8 bytes after
+    /// the first 8 bytes of a string variable, as a kind of placeholder.
     Continuation,
+
+    /// Numeric.
     Numeric,
+
+    /// String.
     String(NonZeroU8),
 }
 
 impl RawWidth {
+    /// Returns the number of value positions corresponding to a variable with
+    /// this type.
     pub fn n_values(&self) -> Option<usize> {
         match self {
             RawWidth::Numeric => Some(1),
             RawWidth::String(width) => Some((width.get() as usize).div_ceil(8)),
-            _ => None,
+            RawWidth::Continuation => None,
         }
     }
 }
@@ -2046,7 +1907,7 @@ impl VariableRecord<RawString> {
                     start_offset,
                     code_offset,
                     code: has_variable_label,
-                })
+                });
             }
         };
 
@@ -2779,7 +2640,7 @@ impl MultipleResponseSet<RawString, RawString> {
                 _ => {
                     return Err(Warning::MultipleResponseSyntaxError(
                         "missing space preceding variable name",
-                    ))
+                    ));
                 }
             }
         }
@@ -2884,27 +2745,7 @@ fn parse_counted_string(input: &[u8]) -> Result<(RawString, &[u8]), Warning> {
     Ok((string.into(), rest))
 }
 
-/// [Level of measurement](https://en.wikipedia.org/wiki/Level_of_measurement).
-#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum Measure {
-    /// Nominal values can only be compared for equality.
-    Nominal,
-
-    /// Ordinal values can be meaningfully ordered.
-    Ordinal,
-
-    /// Scale values can be meaningfully compared for the degree of difference.
-    Scale,
-}
-
 impl Measure {
-    pub fn default_for_type(var_type: VarType) -> Option<Measure> {
-        match var_type {
-            VarType::Numeric => None,
-            VarType::String => Some(Self::Nominal),
-        }
-    }
-
     fn try_decode(source: u32) -> Result<Option<Measure>, Warning> {
         match source {
             0 => Ok(None),
@@ -2914,21 +2755,6 @@ impl Measure {
             _ => Err(Warning::InvalidMeasurement(source)),
         }
     }
-
-    pub fn as_str(&self) -> &'static str {
-        match self {
-            Measure::Nominal => "Nominal",
-            Measure::Ordinal => "Ordinal",
-            Measure::Scale => "Scale",
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum Alignment {
-    Left,
-    Right,
-    Center,
 }
 
 impl Alignment {
@@ -2940,21 +2766,6 @@ impl Alignment {
             _ => Err(Warning::InvalidAlignment(source)),
         }
     }
-
-    pub fn default_for_type(var_type: VarType) -> Self {
-        match var_type {
-            VarType::Numeric => Self::Right,
-            VarType::String => Self::Left,
-        }
-    }
-
-    pub fn as_str(&self) -> &'static str {
-        match self {
-            Alignment::Left => "Left",
-            Alignment::Right => "Right",
-            Alignment::Center => "Center",
-        }
-    }
 }
 
 #[derive(Clone, Debug)]