From 8ff9454a9d53732972e7849c76b48157d63a8f9c Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Fri, 12 Dec 2025 15:42:45 -0800 Subject: [PATCH] work on documenting --- rust/pspp/src/calendar.rs | 7 + rust/pspp/src/dictionary.rs | 4 +- rust/pspp/src/format.rs | 6 + rust/pspp/src/output/pivot/value.rs | 249 ++++++++++++++++++--------- rust/pspp/src/pc.rs | 2 +- rust/pspp/src/por/read.rs | 2 +- rust/pspp/src/spv/read/legacy_bin.rs | 8 +- rust/pspp/src/spv/read/light.rs | 6 +- rust/pspp/src/sys/cooked.rs | 2 +- 9 files changed, 193 insertions(+), 93 deletions(-) diff --git a/rust/pspp/src/calendar.rs b/rust/pspp/src/calendar.rs index 808a0d3bc0..1a90637f56 100644 --- a/rust/pspp/src/calendar.rs +++ b/rust/pspp/src/calendar.rs @@ -14,13 +14,20 @@ // You should have received a copy of the GNU General Public License along with // this program. If not, see . +/// 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 { diff --git a/rust/pspp/src/dictionary.rs b/rust/pspp/src/dictionary.rs index b5d631b98d..73ce62700e 100644 --- a/rust/pspp/src/dictionary.rs +++ b/rust/pspp/src/dictionary.rs @@ -798,7 +798,7 @@ impl<'a> OutputValueLabels<'a> { let mut sorted_value_labels = variable.value_labels.0.iter().collect::>(); 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())), ); diff --git a/rust/pspp/src/format.rs b/rust/pspp/src/format.rs index 744dedbdca..8a0afb3898 100644 --- a/rust/pspp/src/format.rs +++ b/rust/pspp/src/format.rs @@ -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_ diff --git a/rust/pspp/src/output/pivot/value.rs b/rust/pspp/src/output/pivot/value.rs index 25391f0e1b..d5f833c17a 100644 --- a/rust/pspp/src/output/pivot/value.rs +++ b/rust/pspp/src/output/pivot/value.rs @@ -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 . + +//! 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(&self, serializer: S) -> Result +/// 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(pub T); +impl Serialize for BareValue +where + T: Borrow, +{ + fn serialize(&self, serializer: S) -> Result 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, 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(value: &Datum) -> 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(datum: &Datum) -> 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(value: &Datum, 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) -> 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, variable: &Variable) -> Self { + Self::new_datum(&datum.as_encoded(variable.encoding())).with_source_variable(variable) } - pub fn new_number(x: Option) -> Self { - Self::new_number_with_format(x, F8_2) + + pub fn datum(&self) -> Option> { + self.inner.datum() } + + pub fn new_number(number: Option) -> Self { + Self::new(ValueInner::Number(NumberValue::new(number))) + } + pub fn new_integer(x: Option) -> Self { - Self::new_number_with_format(x, F40) + Self::new_number(x).with_format(F40) } pub fn new_text(s: impl Into) -> 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(&self, serializer: S) -> Result + 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) -> 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, } - +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> { + 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 { match self { ValueInner::Number(NumberValue { show, .. }) diff --git a/rust/pspp/src/pc.rs b/rust/pspp/src/pc.rs index 0031452ded..0298cd9f12 100644 --- a/rust/pspp/src/pc.rs +++ b/rust/pspp/src/pc.rs @@ -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), diff --git a/rust/pspp/src/por/read.rs b/rust/pspp/src/por/read.rs index cf79fc8f87..221b6b41c4 100644 --- a/rust/pspp/src/por/read.rs +++ b/rust/pspp/src/por/read.rs @@ -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), diff --git a/rust/pspp/src/spv/read/legacy_bin.rs b/rust/pspp/src/spv/read/legacy_bin.rs index 673df36072..7398d25734 100644 --- a/rust/pspp/src/spv/read/legacy_bin.rs +++ b/rust/pspp/src/spv/read/legacy_bin.rs @@ -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) } } diff --git a/rust/pspp/src/spv/read/light.rs b/rust/pspp/src/spv/read/light.rs index 63ae143a98..5521b208e4 100644 --- a/rust/pspp/src/spv/read/light.rs +++ b/rust/pspp/src/spv/read/light.rs @@ -1201,14 +1201,16 @@ fn parse_format() -> BinResult { 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))) diff --git a/rust/pspp/src/sys/cooked.rs b/rust/pspp/src/sys/cooked.rs index fc850837aa..2b39006329 100644 --- a/rust/pspp/src/sys/cooked.rs +++ b/rust/pspp/src/sys/cooked.rs @@ -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"); -- 2.30.2