work on documenting
authorBen Pfaff <blp@cs.stanford.edu>
Fri, 12 Dec 2025 23:42:45 +0000 (15:42 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Fri, 12 Dec 2025 23:42:45 +0000 (15:42 -0800)
rust/pspp/src/calendar.rs
rust/pspp/src/dictionary.rs
rust/pspp/src/format.rs
rust/pspp/src/output/pivot/value.rs
rust/pspp/src/pc.rs
rust/pspp/src/por/read.rs
rust/pspp/src/spv/read/legacy_bin.rs
rust/pspp/src/spv/read/light.rs
rust/pspp/src/sys/cooked.rs

index 808a0d3bc03781f7fdbf03ae73f224e6369ea364..1a90637f56a4f868dae096bbc53435aaacb02fb4 100644 (file)
 // You should have received a copy of the GNU General Public License along with
 // this program.  If not, see <http://www.gnu.org/licenses/>.
 
+/// Dates and times in PSPP.
+///
+/// PSPP represents dates as the number of seconds since [EPOCH], and times as
+/// the number of seconds since midnight.
 use chrono::{Datelike, Days, Month, NaiveDate, NaiveDateTime, NaiveTime};
 use num::FromPrimitive;
 use thiserror::Error as ThisError;
 
 use crate::format::Settings;
 
+/// The PSPP epoch, 14 Oct 1582, as a [NaiveDate].
 const EPOCH: NaiveDate = NaiveDate::from_ymd_opt(1582, 10, 14).unwrap();
+
+/// The PSPP epoch, 14 Oct 1582, as a [NaiveDateTime].
 const EPOCH_DATETIME: NaiveDateTime = EPOCH.and_time(NaiveTime::MIN);
 
 pub fn date_time_to_pspp(date_time: NaiveDateTime) -> f64 {
index b5d631b98dd49edb3d10019b43957ca6d725159c..73ce62700e2944c3ae23e1f18c001becb4c35d87 100644 (file)
@@ -798,7 +798,7 @@ impl<'a> OutputValueLabels<'a> {
             let mut sorted_value_labels = variable.value_labels.0.iter().collect::<Vec<_>>();
             sorted_value_labels.sort();
             for (datum, label) in sorted_value_labels {
-                let mut value = Value::new_variable_value(variable, datum)
+                let mut value = Value::new_datum_from_variable(datum, &variable)
                     .with_show_value_label(Some(Show::Value));
                 if variable
                     .missing_values()
@@ -809,7 +809,7 @@ impl<'a> OutputValueLabels<'a> {
                 group.push(value);
 
                 data.push(
-                    Value::new_variable_value(variable, datum)
+                    Value::new_datum_from_variable(datum, &variable)
                         .with_show_value_label(Some(Show::Label))
                         .with_value_label(Some(escape_value_label(label.as_str()).into())),
                 );
index 744dedbdcab9852b3ff268cf676ff50059d67787..8a0afb38985fa4d4d41afea071520577b5e6c3d5 100644 (file)
@@ -551,6 +551,12 @@ pub const DATETIME40_0: Format = Format {
     d: 0,
 };
 
+pub const TIME40_0: Format = Format {
+    type_: Type::Time,
+    w: 40,
+    d: 0,
+};
+
 impl Format {
     pub fn type_(self) -> Type {
         self.type_
index 25391f0e1b86fca089d40c08014c72deca2ec5ec..d5f833c17a3c26c52a8b36120411b7430c4aa8b9 100644 (file)
@@ -1,19 +1,31 @@
-use std::{
-    fmt::{Debug, Display, Write},
-    iter::{once, repeat},
-    sync::Arc,
-};
-
-use chrono::NaiveDateTime;
-use serde::{
-    Serialize, Serializer,
-    ser::{SerializeMap, SerializeStruct},
-};
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Data cell contents.
+//!
+//! This module contains [Value], which is the contents of a single pivot table
+//! cell, plus what it in turn contains.
+
+// Warn about missing docs, but not for items declared with `#[cfg(test)]`.
+#![cfg_attr(not(test), warn(missing_docs))]
 
 use crate::{
-    calendar::date_time_to_pspp,
-    data::{Datum, EncodedString},
-    format::{DATETIME40_0, F8_2, F40, Format, Type, UncheckedFormat},
+    calendar::{date_time_to_pspp, time_to_pspp},
+    data::{ByteString, Datum, EncodedString, WithEncoding},
+    format::{DATETIME40_0, F8_2, F40, Format, TIME40_0, Type, UncheckedFormat},
     output::pivot::{
         DisplayMarker, Footnote, FootnoteMarkerType, PivotTable,
         look::{CellStyle, FontStyle},
@@ -22,6 +34,17 @@ use crate::{
     spv::html::Markup,
     variable::{VarType, Variable},
 };
+use chrono::{NaiveDateTime, NaiveTime};
+use serde::{
+    Serialize, Serializer,
+    ser::{SerializeMap, SerializeStruct},
+};
+use std::{
+    borrow::Borrow,
+    fmt::{Debug, Display, Write},
+    iter::{once, repeat},
+    sync::Arc,
+};
 
 /// The content of a single pivot table cell.
 ///
@@ -44,46 +67,61 @@ impl Serialize for Value {
         self.inner.serialize(serializer)
     }
 }
-
-/// Wrapper for [Value] that uses [Value::serialize_bare] for serialization.
-#[derive(Serialize)]
-pub struct BareValue<'a>(#[serde(serialize_with = "Value::serialize_bare")] pub &'a Value);
-
-impl Value {
-    pub fn serialize_bare<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+/// Wrapper for [Value] that serializes in a plain way:
+///
+/// - Numbers: The number.
+/// - Strings: The string.
+/// - Variables: The variable name.
+/// - Text: The localized text string.
+/// - Markup: A string containing HTML for the markup.
+/// - Template: The formatted template string.
+/// - Empty: `()`.
+#[derive(Copy, Clone, Debug, Default, PartialEq)]
+pub struct BareValue<T>(pub T);
+impl<T> Serialize for BareValue<T>
+where
+    T: Borrow<Value>,
+{
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
-        match &self.inner {
+        let value = self.0.borrow();
+        match &value.inner {
             ValueInner::Number(number_value) => number_value.serialize_bare(serializer),
             ValueInner::String(string_value) => string_value.s.serialize(serializer),
             ValueInner::Variable(variable_value) => variable_value.var_name.serialize(serializer),
             ValueInner::Text(text_value) => text_value.localized.serialize(serializer),
             ValueInner::Markup(markup) => markup.serialize(serializer),
-            ValueInner::Template(template_value) => template_value.localized.serialize(serializer),
-            ValueInner::Empty => serializer.serialize_none(),
+            ValueInner::Template(_) => value.display(()).to_string().serialize(serializer),
+            ValueInner::Empty => ().serialize(serializer),
         }
     }
-
+}
+impl Value {
+    /// Constructs a new `Value`, initially with no styling.
+    ///
+    /// Usually one of the other constructors is more convenient.
     pub fn new(inner: ValueInner) -> Self {
         Self {
             inner,
             styling: None,
         }
     }
-    pub fn new_date_time(date_time: NaiveDateTime) -> Self {
-        Self::new_number_with_format(Some(date_time_to_pspp(date_time)), DATETIME40_0)
+
+    /// Constructs a new `Value` as a number whose value is `date_time`
+    /// converted to the [PSPP date representation](crate::calendar).
+    pub fn new_date(date_time: NaiveDateTime) -> Self {
+        Self::new_number(Some(date_time_to_pspp(date_time))).with_format(DATETIME40_0)
     }
-    pub fn new_number_with_format(x: Option<f64>, format: Format) -> Self {
-        Self::new(ValueInner::Number(NumberValue {
-            show: None,
-            format,
-            honor_small: false,
-            value: x,
-            variable: None,
-            value_label: None,
-        }))
+
+    /// Constructs a new `Value` as a number whose value is `time`
+    /// converted to the [PSPP time representation](crate::calendar).
+    pub fn new_time(time: NaiveTime) -> Self {
+        Self::new_number(Some(time_to_pspp(time))).with_format(TIME40_0)
     }
+
+    /// Constructs a new `Value` from `variable`.
     pub fn new_variable(variable: &Variable) -> Self {
         Self::new(ValueInner::Variable(VariableValue {
             show: None,
@@ -91,59 +129,67 @@ impl Value {
             variable_label: variable.label.clone(),
         }))
     }
-    pub fn new_datum<B>(value: &Datum<B>) -> Self
+
+    /// Construct a new `Value` from `datum` with a default format.  Some
+    /// related useful methods are:
+    ///
+    /// - [with_source_variable], to add information about the variable that the
+    ///   datum came from (or use [new_datum_from_variable] as a convenience to
+    ///   combine both).
+    ///
+    /// - [with_format] to override the default format.
+    ///
+    /// [with_source_variable]: Self::with_source_variable
+    /// [new_datum_from_variable]: Self::new_datum_from_variable
+    /// [with_format]: Self::with_format
+    pub fn new_datum<B>(datum: &Datum<B>) -> Self
     where
         B: EncodedString,
     {
-        match value {
+        match datum {
             Datum::Number(number) => Self::new_number(*number),
-            Datum::String(string) => Self::new_user_text(string.as_str()),
-        }
-    }
-    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 format.var_type() {
-                    VarType::Numeric => format,
-                    VarType::String => {
-                        #[cfg(debug_assertions)]
-                        panic!("cannot create numeric pivot value with string format");
-
-                        #[cfg(not(debug_assertions))]
-                        F8_2
-                    }
-                },
-                honor_small: false,
-                value: *number,
-                variable: None,
-                value_label: None,
-            })),
             Datum::String(string) => Self::new(ValueInner::String(StringValue {
                 show: None,
-                hex: format.type_() == Type::AHex,
+                hex: false,
                 s: string.as_str().into_owned(),
                 var_name: None,
                 value_label: None,
             })),
         }
     }
-    pub fn new_variable_value(variable: &Variable, value: &Datum<crate::data::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))
+
+    /// Returns this value with its display format set to `format`.
+    pub fn with_format(self, format: Format) -> Self {
+        Self {
+            inner: self.inner.with_format(format),
+            styling: self.styling,
+        }
+    }
+
+    pub fn with_source_variable(self, variable: &Variable) -> Self {
+        let value_label = self
+            .datum()
+            .and_then(|datum| variable.value_labels.get(&datum).map(String::from));
+        self.with_value_label(value_label)
+            .with_format(variable.print_format)
+            .with_variable_name(Some(variable.name.as_str().into()))
+    }
+
+    /// Construct a new `Value` from `datum`, which is a value of `variable`.
+    pub fn new_datum_from_variable(datum: &Datum<ByteString>, variable: &Variable) -> Self {
+        Self::new_datum(&datum.as_encoded(variable.encoding())).with_source_variable(variable)
     }
-    pub fn new_number(x: Option<f64>) -> Self {
-        Self::new_number_with_format(x, F8_2)
+
+    pub fn datum(&self) -> Option<Datum<&str>> {
+        self.inner.datum()
     }
+
+    pub fn new_number(number: Option<f64>) -> Self {
+        Self::new(ValueInner::Number(NumberValue::new(number)))
+    }
+
     pub fn new_integer(x: Option<f64>) -> Self {
-        Self::new_number_with_format(x, F40)
+        Self::new_number(x).with_format(F40)
     }
     pub fn new_text(s: impl Into<String>) -> Self {
         Self::new_user_text(s)
@@ -268,6 +314,15 @@ impl Value {
     pub const fn is_empty(&self) -> bool {
         self.inner.is_empty() && self.styling.is_none()
     }
+    /// Serializes this value in a plain way, like [BareValue].  This function
+    /// can be used on a field as `#[serde(serialize_with =
+    /// Value::serialize_bare)]`.
+    pub fn serialize_bare<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        BareValue(self).serialize(serializer)
+    }
 }
 
 impl From<&str> for Value {
@@ -498,6 +553,19 @@ impl Serialize for NumberValue {
 }
 
 impl NumberValue {
+    pub fn new(value: Option<f64>) -> Self {
+        Self {
+            value,
+            format: F8_2,
+            show: None,
+            honor_small: false,
+            variable: None,
+            value_label: None,
+        }
+    }
+    pub fn with_format(self, format: Format) -> Self {
+        Self { format, ..self }
+    }
     pub fn display<'a>(
         &self,
         display: &DisplayValue<'a>,
@@ -537,18 +605,12 @@ impl NumberValue {
             && number >= -(1i64 << 53) as f64
             && number <= (1i64 << 53) as f64
         {
-            (number as u64).serialize(serializer)
+            Some(number as u64).serialize(serializer)
         } else {
             self.value.serialize(serializer)
         }
     }
 }
-
-#[derive(Serialize)]
-pub struct BareNumberValue<'a>(
-    #[serde(serialize_with = "NumberValue::serialize_bare")] pub &'a NumberValue,
-);
-
 /// A string value and how to display it.
 #[derive(Clone, Debug, Serialize, PartialEq)]
 pub struct StringValue {
@@ -572,7 +634,14 @@ pub struct StringValue {
     /// The value label associated with `s`, if any.
     pub value_label: Option<String>,
 }
-
+impl StringValue {
+    pub fn with_format(self, format: Format) -> Self {
+        Self {
+            hex: format.type_() == Type::AHex,
+            ..self
+        }
+    }
+}
 /// A variable name.
 #[derive(Clone, Debug, Serialize, PartialEq)]
 pub struct VariableValue {
@@ -836,6 +905,22 @@ impl ValueInner {
     pub const fn is_empty(&self) -> bool {
         matches!(self, Self::Empty)
     }
+    pub fn with_format(self, format: Format) -> Self {
+        match self {
+            ValueInner::Number(number_value) => Self::Number(number_value.with_format(format)),
+            ValueInner::String(string_value) => Self::String(string_value.with_format(format)),
+            _ => self,
+        }
+    }
+
+    pub fn datum(&self) -> Option<Datum<&str>> {
+        match self {
+            ValueInner::Number(number_value) => Some(Datum::Number(number_value.value)),
+            ValueInner::String(string_value) => Some(Datum::String(&string_value.s)),
+            _ => None,
+        }
+    }
+
     fn show(&self) -> Option<Show> {
         match self {
             ValueInner::Number(NumberValue { show, .. })
index 0031452ded79e15f7003aea25e3ca5f0bad35086..0298cd9f1254fe00d6e8606083920864dcc58e50 100644 (file)
@@ -137,7 +137,7 @@ impl From<&Metadata> for PivotTable {
             value: MetadataValue::Group(vec![
                 MetadataEntry {
                     name: Value::new_user_text("Created"),
-                    value: MetadataValue::new_leaf(Value::new_date_time(value.creation)),
+                    value: MetadataValue::new_leaf(Value::new_date(value.creation)),
                 },
                 maybe_string("Product", &value.product),
                 maybe_string("File Name", &value.filename),
index cf79fc8f8789a6406a12d9d22d06894a978b3547..221b6b41c4195c257ae8bfbb0b691410d522a2d3 100644 (file)
@@ -122,7 +122,7 @@ impl From<&Metadata> for PivotTable {
                 MetadataEntry {
                     name: Value::new_user_text("Created"),
                     value: MetadataValue::Leaf(
-                        value.creation.map(Value::new_date_time).unwrap_or_default(),
+                        value.creation.map(Value::new_date).unwrap_or_default(),
                     ),
                 },
                 maybe_string("Product", &value.product),
index 673df36072374434dcdc1c6c612ceb0ea4f118a3..7398d257347d9227760d5e153985c75a2bce99f5 100644 (file)
@@ -8,7 +8,6 @@ use chrono::{NaiveDateTime, NaiveTime};
 use encoding_rs::UTF_8;
 
 use crate::{
-    calendar::{date_time_to_pspp, time_to_pspp},
     data::Datum,
     format::{Category, Format},
     output::pivot::value::Value,
@@ -104,15 +103,16 @@ impl DataValue {
             && 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)
+            Value::new_date(date_time)
         } 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)
+            Value::new_time(time)
         } else {
-            Value::new_datum_with_format(&self.value, format)
+            Value::new_datum(&self.value)
         }
+        .with_format(format)
     }
 }
 
index 63ae143a98558d4f99d7cd1e6776f2ed1dfcdd08..5521b208e4db585c21f46b11dc8cbdd0910fb393 100644 (file)
@@ -1201,14 +1201,16 @@ fn parse_format() -> BinResult<Format> {
 
 impl ValueNumber {
     fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> value::Value {
-        value::Value::new_number_with_format((self.x != -f64::MAX).then_some(self.x), self.format)
+        value::Value::new_number((self.x != -f64::MAX).then_some(self.x))
+            .with_format(self.format)
             .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
     }
 }
 
 impl ValueVarNumber {
     fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> value::Value {
-        value::Value::new_number_with_format((self.x != -f64::MAX).then_some(self.x), self.format)
+        value::Value::new_number((self.x != -f64::MAX).then_some(self.x))
+            .with_format(self.format)
             .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
             .with_value_label(self.value_label.decode_optional(encoding))
             .with_variable_name(Some(self.var_name.decode(encoding)))
index fc850837aac0ee351d48252e8e8bc01fd4021834..2b39006329469f593ede602551890f9a114ae8f2 100644 (file)
@@ -1440,7 +1440,7 @@ impl Metadata {
         let mut values = Vec::new();
 
         group.push("Created");
-        values.push(Value::new_date_time(self.creation));
+        values.push(Value::new_date(self.creation));
 
         let mut product = Group::new("Writer");
         product.push("Product");