sync::LazyLock,
};
+use chrono::{Datelike, Local, NaiveDate};
use encoding_rs::Encoding;
use enum_map::{Enum, EnumMap};
use libm::frexp;
use smallstr::SmallString;
-use smallvec::SmallVec;
+use smallvec::{Array, SmallVec};
use thiserror::Error as ThisError;
use unicode_width::UnicodeWidthStr;
use crate::{
+ calendar::{calendar_offset_to_gregorian, day_of_year, month_name, short_month_name},
dictionary::{Value, VarWidth},
raw::{self, VarType},
settings::Settings as PsppSettings,
}
}
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Epoch(pub i32);
+
+impl Default for Epoch {
+ fn default() -> Self {
+ static DEFAULT: LazyLock<Epoch> = LazyLock::new(|| Epoch(Local::now().year() - 69));
+ *DEFAULT
+ }
+}
+
+impl Display for Epoch {
+ fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+ write!(f, "{}", self.0)
+ }
+}
+
#[derive(Clone, Debug, Default)]
pub struct Settings {
- pub epoch: Option<i32>,
+ pub epoch: Epoch,
/// Either `'.'` or `','`.
pub decimal: Decimal,
Type::PIBHex => self.pibhex(f, number),
Type::RBHex => self.rbhex(f, number),
- Type::Date |
- Type::ADate |
- Type::EDate |
- Type::JDate |
- Type::SDate |
- Type::QYr |
- Type::MoYr |
- Type::WkYr |
- Type::DateTime |
- Type::YMDHMS |
- Type::MTime |
- Type::Time |
- Type::DTime |
- Type::WkDay => self.date(f, number),
- Type::Month => todo!(),
- Type::A => todo!(),
- Type::AHex => todo!(),
+ Type::Date
+ | Type::ADate
+ | Type::EDate
+ | Type::JDate
+ | Type::SDate
+ | Type::QYr
+ | Type::MoYr
+ | Type::WkYr
+ | Type::DateTime
+ | Type::YMDHMS
+ | Type::MTime
+ | Type::Time
+ | Type::DTime
+ | Type::WkDay => self.date(f, number),
+ Type::Month => self.month(f, number),
+ Type::A | Type::AHex => unreachable!(),
}
}
}
impl<'a> DisplayValue<'a> {
+ pub fn new(format: Format, value: &'a Value, encoding: &'static Encoding) -> Self {
+ Self {
+ format,
+ value,
+ encoding,
+ }
+ }
fn number(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
if number.is_finite() {
let style = PsppSettings::global()
// Figure out number of characters we can use for the fraction, if any.
// (If that turns out to be `1`, then we'll output a decimal point
// without any digits following.)
- let mut fraction_width =
- min(self.format.d as usize + 1, self.format.w() - width).min(16);
+ let mut fraction_width = min(self.format.d as usize + 1, self.format.w() - width).min(16);
if self.format.type_ != Type::E && fraction_width == 1 {
fraction_width = 0;
}
// Rust always uses `.` as the decimal point. Translate to `,` if
// necessary.
if style.decimal == Decimal::Comma {
- // SAFETY: This only changes only one ASCII character (`.`) to
- // another ASCII character (`,`).
- unsafe {
- if let Some(dot) = output.as_bytes_mut().iter_mut().find(|c| **c == b'.') {
- *dot = b',';
- }
- }
+ fix_decimal_point(&mut output);
}
// Make exponent have exactly three digits, plus sign.
}
fn date(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
- let template = self.format.type_.date_template(self.format.w()).unwrap();
- if self.format.type_.category() == Category::Date {
- if number < 0.0 {
- return self.missing(f);
+ const MINUTE: f64 = 60.0;
+ const HOUR: f64 = 60.0 * 60.0;
+ const DAY: f64 = 60.0 * 60.0 * 24.0;
+
+ let (date, mut time) = match self.format.type_.category() {
+ Category::Date => {
+ if number < 0.0 {
+ return self.missing(f);
+ }
+ let Some(date) = calendar_offset_to_gregorian(number / DAY) else {
+ return self.missing(f);
+ };
+ (date, number % DAY)
}
-
+ Category::Time => (NaiveDate::MIN, number),
+ _ => unreachable!(),
+ };
+
+ let mut output = SmallString::<[u8; 40]>::new();
+ let mut template = self
+ .format
+ .type_
+ .date_template(self.format.w())
+ .unwrap()
+ .bytes()
+ .peekable();
+ while let Some(c) = template.next() {
+ let mut count = 1;
+ while template.next_if_eq(&c).is_some() {
+ count += 1;
+ }
+ match c {
+ b'd' if count < 3 => write!(&mut output, "{:02}", date.day()).unwrap(),
+ b'd' => write!(&mut output, "{:03}", day_of_year(date).unwrap_or(1)).unwrap(),
+ b'm' if count < 3 => write!(&mut output, "{:02}", date.month()).unwrap(),
+ b'm' => write!(&mut output, "{}", short_month_name(date.month()).unwrap()).unwrap(),
+ b'y' if count >= 4 => {
+ let year = date.year();
+ if year <= 9999 {
+ write!(&mut output, "{year:04}").unwrap();
+ } else if self.format.type_ == Type::DateTime
+ || self.format.type_ == Type::YMDHMS
+ {
+ write!(&mut output, "****").unwrap();
+ } else {
+ return self.overflow(f);
+ }
+ }
+ b'y' => {
+ let epoch = PsppSettings::global().formats.epoch.0;
+ let offset = date.year() - epoch;
+ if offset < 0 || offset > 99 {
+ return self.overflow(f);
+ }
+ write!(&mut output, "{offset:02}").unwrap();
+ }
+ b'q' => write!(&mut output, "{}", date.month0() / 3 + 1).unwrap(),
+ b'w' => write!(
+ &mut output,
+ "{:2}",
+ (day_of_year(date).unwrap_or(1) - 1) / 7 + 1
+ )
+ .unwrap(),
+ b'D' => {
+ if time < 0.0 {
+ output.push('-');
+ }
+ time = time.abs();
+ write!(&mut output, "{:1$.0}", (time / DAY).floor(), count).unwrap();
+ time %= DAY;
+ }
+ b'H' => {
+ if time < 0.0 {
+ output.push('-');
+ }
+ time = time.abs();
+ write!(&mut output, "{:1$.0}", (time / HOUR).floor(), count).unwrap();
+ time %= HOUR;
+ }
+ b'M' => {
+ if time < 0.0 {
+ output.push('-');
+ }
+ time = time.abs();
+ write!(&mut output, "{:02.0}", (time / MINUTE).floor()).unwrap();
+ time %= MINUTE;
+
+ let excess_width = self.format.w() as isize - output.len() as isize;
+ if excess_width < 0 || (self.format.type_ == Type::MTime && excess_width < 3) {
+ return self.overflow(f);
+ }
+ if excess_width == 3
+ || excess_width == 4
+ || (excess_width >= 5 && self.format.d == 0)
+ {
+ write!(&mut output, ":{:02.0}", time.floor()).unwrap();
+ } else if excess_width >= 5 {
+ let d = min(self.format.d(), excess_width as usize);
+ let w = d + 3;
+ write!(&mut output, ":{:02$.*}", d, number, w).unwrap();
+ if PsppSettings::global().formats.decimal == Decimal::Comma {
+ fix_decimal_point(&mut output);
+ }
+ }
+ }
+ c if count == 1 => output.push(c as char),
+ _ => unreachable!(),
+ }
+ }
+ write!(f, "{:>.*}", self.format.w(), &output)
+ }
+
+ fn month(&self, f: &mut Formatter<'_>, number: f64) -> FmtResult {
+ if let Some(month) = month_name(number as u32) {
+ write!(f, "{month:.*}", self.format.w())
+ } else {
+ self.missing(f)
}
- todo!()
}
}
.copied()
.unwrap_or_else(|| 256.0_f64.powi(x as i32))
}
+
+fn fix_decimal_point<A>(s: &mut SmallString<A>)
+where
+ A: Array<Item = u8>,
+{
+ // SAFETY: This only changes only one ASCII character (`.`) to
+ // another ASCII character (`,`).
+ unsafe {
+ if let Some(dot) = s.as_bytes_mut().iter_mut().find(|c| **c == b'.') {
+ *dot = b',';
+ }
+ }
+}