Dedupe the 400-year-cycle UTC conversion and the EPUB package writer (#1537)

* Dedupe the 400-year-cycle UTC conversion and the EPUB package writer

SonarCloud flagged new-code duplication on main (17%): five copies of the
"move the instant into chrono's range by whole 400-year cycles" conversion
and two copies of the EPUB OPF/container/zip writer.

- designcraft-doc gains a `time` module: `date_parts(days)` and
  `utc_parts(secs, nanos)`. App logging, EPUB `iso8601`, PDF `civil`, PPTX
  `civil` and text-variable `civil` are now thin callers; each keeps its own
  format string and year clamp. The app reaches it through
  `designcraft_engine::doc`, so its direct chrono dependency is dropped.
- EPUB: `package_opf` + `write_package` shared by fixed-layout and
  reflowable export (same entry order and compression).
- Boundary tests on the helper: pre-1970, year 9999 -> 10000, i64 extremes,
  nanos past a second.

Output is unchanged: existing date tests pass, and fixed/reflowable EPUBs
(with/without cover, author, pre-1970 and i64::MAX dates) are byte-identical
to origin/main.

* time: drop the unreachable i128 clamp and match arms in utc_parts

Whole 400-year cycles times 400 stay under 2.6e16 for any i64 day count,
so the year can't overflow i64 and the i128 + saturate was dead code
(carried over from the copies this replaced). The date and time lookups
can't fail either; each keeps one unwrap_or_default instead of a match
with a fake 1970 fallback. Output and tests unchanged.
This commit is contained in:
Zuujis authored and GitHub committed 2026-10-21 21:46:38 -04:00
1 parent 2aa5da5950
commit 438d7b6d79
9 files changed
+140 -146

No files matched your search

Generated
-1
View File
@@ -945,7 +945,6 @@ name = "designcraft"
version = "0.11.0"
dependencies = [
"arboard",
"chrono",
"designcraft-engine",
"designcraft-ui-egui",
"eframe",
-1
View File
@@ -18,7 +18,6 @@ egui = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
chrono = { workspace = true }
[build-dependencies]
# Icon + VERSIONINFO for designcraft.exe; build.rs only uses it when the target is Windows.
+1 -21
View File
@@ -97,31 +97,11 @@ pub fn timestamp(t: SystemTime) -> String {
// Pre-1970 is clamped to the epoch by `unwrap_or_default` above. `as_secs` is `u64`; a value
// past `i64::MAX` seconds saturates rather than falling back to the epoch.
let secs = i64::try_from(since.as_secs()).unwrap_or(i64::MAX);
let (y, t) = utc_parts(secs, since.subsec_nanos());
let (y, t) = designcraft_engine::doc::time::utc_parts(secs, since.subsec_nanos());
// `{:04}` rather than `%Y`, which writes a `+` before years past 9999.
format!("{y:04}-{}", t.format("%m-%dT%H:%M:%S%.3fZ"))
}
/// Unix seconds (+ nanos) → (proleptic Gregorian year, the same wall-clock instant moved into
/// chrono's range by whole 400-year cycles). The calendar repeats every 400 years (146_097 days),
/// so month, day and time of day come from chrono and only the year is carried, which keeps every
/// `i64` exact. Replaces the old hand-rolled `civil_from_days`; chrono is the only calendar logic.
fn utc_parts(secs: i64, nanos: u32) -> (i64, chrono::NaiveDateTime) {
use chrono::Datelike;
let (days, sod) = (secs.div_euclid(86_400), secs.rem_euclid(86_400));
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
// `rest + 719_163` (1970-01-01 is CE day 719_163) is always a valid CE day, and `sod` < 86_400.
let date = i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt);
let time = u32::try_from(sod).ok().and_then(|s| chrono::NaiveTime::from_num_seconds_from_midnight_opt(s, nanos.min(999_999_999)));
match (date, time) {
(Some(d), Some(t)) => {
let y = i128::from(d.year()) + i128::from(cycles) * 400;
(i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }), d.and_time(t))
}
_ => (1970, chrono::NaiveDateTime::default()), // unreachable
}
}
fn numbered(dir: &Path, n: usize) -> PathBuf {
if n == 0 { dir.join(LOG_FILE) } else { dir.join(format!("designcraft.{n}.log")) }
}
+1
View File
@@ -35,6 +35,7 @@ pub mod story;
pub mod styles;
mod swatch_refs;
pub mod table;
pub mod time;
pub mod vars;
pub mod xref;
+74
View File
@@ -0,0 +1,74 @@
//! Unix time → proleptic Gregorian calendar, exact for every `i64`.
//!
//! chrono is the only calendar logic. `NaiveDate` covers about ±262143 years, but clocks and files
//! can hold instants past that, so these extend its range: the Gregorian calendar repeats every
//! 400 years (146_097 days), so the instant is moved into chrono's range by whole cycles, chrono
//! gives the month, day and time of day, and the cycles are added back to the year (at most about
//! 2.5e16, so it fits an `i64`). Callers own only their output format and any year clamp.
use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime};
/// Days since 1970-01-01 → (proleptic Gregorian year, a chrono date with the same month and day;
/// its own year is not the real one).
pub fn date_parts(days: i64) -> (i64, NaiveDate) {
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
// 1970-01-01 is CE day 719_163; one cycle on from it is well inside chrono's range.
let d = NaiveDate::from_num_days_from_ce_opt(rest as i32 + 719_163).unwrap_or_default();
(i64::from(d.year()) + cycles * 400, d)
}
/// Unix seconds (+ nanos, clamped to 999_999_999) → (proleptic Gregorian year, a chrono date and
/// time with the same month, day and time of day; its own year is not the real one).
pub fn utc_parts(secs: i64, nanos: u32) -> (i64, NaiveDateTime) {
let (days, sod) = (secs.div_euclid(86_400), secs.rem_euclid(86_400));
let (y, d) = date_parts(days);
let t = NaiveTime::from_num_seconds_from_midnight_opt(sod as u32, nanos.min(999_999_999)).unwrap_or_default();
(y, d.and_time(t))
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Timelike;
fn ymdhms(secs: i64) -> (i64, u32, u32, u32, u32, u32) {
let (y, t) = utc_parts(secs, 0);
(y, t.month(), t.day(), t.hour(), t.minute(), t.second())
}
#[test]
fn epoch_and_ordinary_dates() {
assert_eq!(ymdhms(0), (1970, 1, 1, 0, 0, 0));
assert_eq!(ymdhms(951_782_400), (2000, 2, 29, 0, 0, 0));
assert_eq!(ymdhms(1_791_504_000 + 3_723), (2026, 10, 9, 1, 2, 3));
}
#[test]
fn before_1970() {
assert_eq!(ymdhms(-1), (1969, 12, 31, 23, 59, 59));
assert_eq!(ymdhms(-86_400), (1969, 12, 31, 0, 0, 0));
assert_eq!(ymdhms(-10_000_000_000_000), (-314918, 8, 13, 6, 13, 20));
}
#[test]
fn year_9999_rolls_into_10000() {
assert_eq!(ymdhms(253_402_300_799), (9999, 12, 31, 23, 59, 59));
assert_eq!(ymdhms(253_402_300_800), (10000, 1, 1, 0, 0, 0));
}
#[test]
fn i64_extremes_stay_exact() {
assert_eq!(ymdhms(i64::MAX), (292277026596, 12, 4, 15, 30, 7));
assert_eq!(ymdhms(i64::MIN), (-292277022657, 1, 27, 8, 29, 52));
// Days (not seconds) at the extremes: whole cycles still fit, so the year can't overflow.
let ((hi, _), (lo, _)) = (date_parts(i64::MAX), date_parts(i64::MIN));
assert!(hi > 25_000_000_000_000_000 && hi < i64::MAX && lo < -25_000_000_000_000_000 && lo > i64::MIN);
}
#[test]
fn nanos_past_a_second_are_clamped() {
assert_eq!(utc_parts(0, 999_999_999).1.nanosecond(), 999_999_999);
assert_eq!(utc_parts(0, 1_500_000_000).1.nanosecond(), 999_999_999);
assert_eq!(utc_parts(0, u32::MAX).1.second(), 0);
}
}
+4 -16
View File
@@ -126,24 +126,12 @@ pub fn now() -> i64 {
}
}
/// (year, month 1–12, day 1–31) from days since 1970-01-01.
///
/// `NaiveDate` only covers about ±262143 years, but the system clock can run past it, so this
/// stays exact for every `days`. The Gregorian calendar repeats every 400 years (146_097 days):
/// chrono converts the day within its cycle and whole cycles go back into the year (an i128 so no
/// `days` overflows), which matches the proleptic Gregorian answer the old hand-rolled code gave.
/// 1970-01-01 is `num_days_from_ce` 719_163.
/// (year, month 1–12, day 1–31) from days since 1970-01-01, exact for every `days`
/// (see [`crate::time::date_parts`]).
fn civil(days: i64) -> (i64, u32, u32) {
use chrono::Datelike;
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
match i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt) {
Some(d) => {
let y = i128::from(d.year()) + i128::from(cycles) * 400;
(i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }), d.month(), d.day())
}
// Unreachable: `rest + 719_163` is always a valid CE day.
None => (1970, 1, 1),
}
let (y, d) = crate::time::date_parts(days);
(y, d.month(), d.day())
}
/// Days since 1970-01-01 of a civil date (inverse of [`civil`]), exact for any year.
+56 -81
View File
@@ -127,30 +127,11 @@ fn font_file(k: usize, f: &EmbeddedFont) -> (String, &'static str) {
/// instant is moved into range by whole cycles and only the year is carried (matches the old
/// `civil_from_days`), rather than resetting to the epoch.
fn iso8601(t: i64) -> String {
let (y, dt) = utc_parts(t, 0);
let (y, dt) = designcraft_doc::time::utc_parts(t, 0);
// `{:04}` rather than `%Y`, which writes a `+` before years past 9999.
format!("{y:04}-{}", dt.format("%m-%dT%H:%M:%SZ"))
}
/// Unix seconds (+ nanos) → (proleptic Gregorian year, the instant moved into chrono's range by
/// whole 400-year cycles). Month, day and time of day come from chrono; only the year is carried,
/// which keeps every `i64` exact. chrono is the only calendar logic.
fn utc_parts(secs: i64, nanos: u32) -> (i64, chrono::NaiveDateTime) {
use chrono::Datelike;
let (days, sod) = (secs.div_euclid(86_400), secs.rem_euclid(86_400));
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
// `rest + 719_163` (1970-01-01 is CE day 719_163) is always a valid CE day, and `sod` < 86_400.
let date = i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt);
let time = u32::try_from(sod).ok().and_then(|s| chrono::NaiveTime::from_num_seconds_from_midnight_opt(s, nanos.min(999_999_999)));
match (date, time) {
(Some(d), Some(t)) => {
let y = i128::from(d.year()) + i128::from(cycles) * 400;
(i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }), d.and_time(t))
}
_ => (1970, chrono::NaiveDateTime::default()), // unreachable
}
}
/// The package's `dcterms:modified`: the given time, else the document's last save or creation.
fn modified(doc: &Document, opts: &EpubOptions) -> String {
let t = opts.modified.or((doc.modified > 0).then_some(doc.modified)).or((doc.created > 0).then_some(doc.created));
@@ -206,8 +187,6 @@ pub struct FixedPage {
/// A pre-paginated (fixed-layout) EPUB: one XHTML page per document page.
pub fn export_fixed_epub(doc: &Document, pages: &[FixedPage], opts: &EpubOptions) -> Result<Vec<u8>, EpubError> {
let io = |e: std::io::Error| EpubError(e.to_string());
let zerr = |e: zip::result::ZipError| EpubError(e.to_string());
let title = opts.title.clone().unwrap_or_else(|| doc.title.clone());
let lang = language_tag(&opts.language);
let id = identifier(doc, opts, &title);
@@ -239,12 +218,41 @@ pub fn export_fixed_epub(doc: &Document, pages: &[FixedPage], opts: &EpubOptions
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\" xml:lang=\"{lang}\">\n<head><meta charset=\"utf-8\"/><title>{}</title></head>\n<body><nav epub:type=\"page-list\" id=\"toc\"><h1>Pages</h1><ol>{nav_items}</ol></nav></body>\n</html>\n",
xml_text(&title)
);
let author = opts.author.as_deref().map(|a| format!("<dc:creator>{}</dc:creator>", xml_text(a))).unwrap_or_default();
let opf = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<package xmlns=\"http://www.idpf.org/2007/opf\" version=\"3.0\" unique-identifier=\"bookid\" xml:lang=\"{lang}\" prefix=\"rendition: http://www.idpf.org/vocab/rendition/#\">\n<metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n<dc:identifier id=\"bookid\">{}</dc:identifier>\n<dc:title>{}</dc:title>\n<dc:language>{lang}</dc:language>{author}\n<meta property=\"dcterms:modified\">{modified}</meta>\n<meta property=\"rendition:layout\">pre-paginated</meta>\n<meta property=\"rendition:spread\">auto</meta>\n<meta name=\"original-resolution\" content=\"{vw}x{vh}\"/>\n<meta name=\"generator\" content=\"DesignCraft\"/>\n</metadata>\n<manifest>\n{manifest}</manifest>\n<spine>\n{spine}</spine>\n</package>\n",
xml_text(&id),
xml_text(&title)
let rendition = format!(
"\n<meta property=\"rendition:layout\">pre-paginated</meta>\n<meta property=\"rendition:spread\">auto</meta>\n<meta name=\"original-resolution\" content=\"{vw}x{vh}\"/>"
);
let head = PackageHead { lang, id: &id, title: &title, author: opts.author.as_deref(), modified: &modified };
let opf = package_opf(&head, " prefix=\"rendition: http://www.idpf.org/vocab/rendition/#\"", &rendition, &manifest, &spine);
let entries = files.iter().map(|(name, data)| (name.clone(), data.as_slice(), name.ends_with(".png")));
write_package(&opf, &nav, entries)
}
/// The package metadata both EPUB flavours share.
struct PackageHead<'a> {
lang: &'a str,
id: &'a str,
title: &'a str,
author: Option<&'a str>,
modified: &'a str,
}
/// The OPF package document. `prefix` goes on `<package>` (with its leading space), `meta` after
/// `dcterms:modified` (with its leading newline); `manifest` and `spine` are the item lines.
fn package_opf(h: &PackageHead, prefix: &str, meta: &str, manifest: &str, spine: &str) -> String {
let PackageHead { lang, id, title, author, modified } = h;
let author = author.map(|a| format!("<dc:creator>{}</dc:creator>", xml_text(a))).unwrap_or_default();
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<package xmlns=\"http://www.idpf.org/2007/opf\" version=\"3.0\" unique-identifier=\"bookid\" xml:lang=\"{lang}\"{prefix}>\n<metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n<dc:identifier id=\"bookid\">{}</dc:identifier>\n<dc:title>{}</dc:title>\n<dc:language>{lang}</dc:language>{author}\n<meta property=\"dcterms:modified\">{modified}</meta>{meta}\n<meta name=\"generator\" content=\"DesignCraft\"/>\n</metadata>\n<manifest>\n{manifest}</manifest>\n<spine>\n{spine}</spine>\n</package>\n",
xml_text(id),
xml_text(title)
)
}
/// Zips an EPUB: the stored `mimetype`, `container.xml`, the OPF and nav, then `entries` in order
/// as (path, bytes, stored uncompressed).
fn write_package<'a>(opf: &str, nav: &str, entries: impl IntoIterator<Item = (String, &'a [u8], bool)>) -> Result<Vec<u8>, EpubError> {
let io = |e: std::io::Error| EpubError(e.to_string());
let zerr = |e: zip::result::ZipError| EpubError(e.to_string());
let container = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\"><rootfiles><rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/></rootfiles></container>\n";
let mut buf = std::io::Cursor::new(Vec::new());
{
@@ -259,9 +267,8 @@ pub fn export_fixed_epub(doc: &Document, pages: &[FixedPage], opts: &EpubOptions
z.start_file(name, deflate).map_err(zerr)?;
z.write_all(data).map_err(io)?;
}
for (name, data) in &files {
let o = if name.ends_with(".png") { stored } else { deflate };
z.start_file(name.as_str(), o).map_err(zerr)?;
for (name, data, store) in entries {
z.start_file(name, if store { stored } else { deflate }).map_err(zerr)?;
z.write_all(data).map_err(io)?;
}
z.finish().map_err(zerr)?;
@@ -1379,8 +1386,6 @@ fn base64(data: &[u8]) -> String {
/// Export the document as a reflowable EPUB 3.
pub fn export_epub(doc: &Document, opts: &EpubOptions) -> Result<Vec<u8>, EpubError> {
let io = |e: std::io::Error| EpubError(e.to_string());
let zerr = |e: zip::result::ZipError| EpubError(e.to_string());
let title = opts.title.clone().unwrap_or_else(|| doc.title.clone());
let id = identifier(doc, opts, &title);
let modified = modified(doc, opts);
@@ -1441,56 +1446,26 @@ pub fn export_epub(doc: &Document, opts: &EpubOptions) -> Result<Vec<u8>, EpubEr
);
}
let cover_spine = if opts.cover.is_some() { "<itemref idref=\"cover\"/>\n" } else { "" };
let author = opts.author.as_deref().map(|a| format!("<dc:creator>{}</dc:creator>", xml_text(a))).unwrap_or_default();
let opf = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<package xmlns=\"http://www.idpf.org/2007/opf\" version=\"3.0\" unique-identifier=\"bookid\" xml:lang=\"{lang}\">\n<metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n<dc:identifier id=\"bookid\">{}</dc:identifier>\n<dc:title>{}</dc:title>\n<dc:language>{lang}</dc:language>{author}\n<meta property=\"dcterms:modified\">{modified}</meta>\n<meta name=\"generator\" content=\"DesignCraft\"/>\n</metadata>\n<manifest>\n{manifest}</manifest>\n<spine>\n{cover_spine}{spine}</spine>\n</package>\n",
xml_text(&id),
xml_text(&title)
);
let container = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\"><rootfiles><rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/></rootfiles></container>\n";
let mut buf = std::io::Cursor::new(Vec::new());
{
let mut z = zip::ZipWriter::new(&mut buf);
let stored = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
let deflate = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
z.start_file("mimetype", stored).map_err(zerr)?;
z.write_all(b"application/epub+zip").map_err(io)?;
for (name, data) in [
("META-INF/container.xml", container.as_bytes()),
("OEBPS/content.opf", opf.as_bytes()),
("OEBPS/nav.xhtml", nav.as_bytes()),
("OEBPS/style.css", stylesheet_with(doc, &opts.generic_families, &opts.fonts).as_bytes()),
] {
z.start_file(name, deflate).map_err(zerr)?;
z.write_all(data).map_err(io)?;
}
for (k, c) in chapters.iter().enumerate() {
z.start_file(format!("OEBPS/{}", chapter_file(k)), deflate).map_err(zerr)?;
z.write_all(c.as_bytes()).map_err(io)?;
}
for (name, (_, data)) in &images {
z.start_file(format!("OEBPS/{name}"), stored).map_err(zerr)?;
z.write_all(data).map_err(io)?;
}
for (k, f) in opts.fonts.iter().enumerate() {
z.start_file(format!("OEBPS/{}", font_file(k, f).0), deflate).map_err(zerr)?;
z.write_all(&f.data).map_err(io)?;
}
if let Some(c) = &opts.cover {
z.start_file(format!("OEBPS/{}", cover_file(c).0), stored).map_err(zerr)?;
z.write_all(c).map_err(io)?;
let xhtml = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\" xml:lang=\"{lang}\">\n<head><meta charset=\"utf-8\"/><title>{}</title><style>body{{margin:0;text-align:center}} img{{max-width:100%;max-height:100vh}}</style></head>\n<body epub:type=\"cover\"><img src=\"{}\" alt=\"{}\"/></body>\n</html>\n",
xml_text(&title),
cover_file(c).0,
xml_attr(&title)
);
z.start_file("OEBPS/cover.xhtml", deflate).map_err(zerr)?;
z.write_all(xhtml.as_bytes()).map_err(io)?;
}
z.finish().map_err(zerr)?;
let head = PackageHead { lang, id: &id, title: &title, author: opts.author.as_deref(), modified: &modified };
let opf = package_opf(&head, "", "", &manifest, &format!("{cover_spine}{spine}"));
let css = stylesheet_with(doc, &opts.generic_families, &opts.fonts);
let mut entries: Vec<(String, &[u8], bool)> = vec![("OEBPS/style.css".into(), css.as_bytes(), false)];
entries.extend(chapters.iter().enumerate().map(|(k, c)| (format!("OEBPS/{}", chapter_file(k)), c.as_bytes(), false)));
entries.extend(images.iter().map(|(name, (_, data))| (format!("OEBPS/{name}"), *data, true)));
entries.extend(opts.fonts.iter().enumerate().map(|(k, f)| (format!("OEBPS/{}", font_file(k, f).0), f.data.as_slice(), false)));
let cover_xhtml = opts.cover.as_ref().map(|c| {
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\" xml:lang=\"{lang}\">\n<head><meta charset=\"utf-8\"/><title>{}</title><style>body{{margin:0;text-align:center}} img{{max-width:100%;max-height:100vh}}</style></head>\n<body epub:type=\"cover\"><img src=\"{}\" alt=\"{}\"/></body>\n</html>\n",
xml_text(&title),
cover_file(c).0,
xml_attr(&title)
)
});
if let (Some(c), Some(xhtml)) = (&opts.cover, &cover_xhtml) {
entries.push((format!("OEBPS/{}", cover_file(c).0), c.as_slice(), true));
entries.push(("OEBPS/cover.xhtml".into(), xhtml.as_bytes(), false));
}
Ok(buf.into_inner())
write_package(&opf, &nav, entries)
}
#[cfg(test)]
+2 -13
View File
@@ -442,19 +442,8 @@ fn date_time(t: i64) -> krilla::metadata::DateTime {
/// `civil`), rather than resetting to the epoch. `date_time` applies the PDF `0..=9999` year clamp.
pub(crate) fn civil(t: i64) -> (i64, u8, u8, u8, u8, u8) {
use chrono::{Datelike, Timelike};
let (days, sod) = (t.div_euclid(86_400), t.rem_euclid(86_400));
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
// `rest + 719_163` (1970-01-01 is CE day 719_163) is always a valid CE day, and `sod` < 86_400.
let date = i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt);
let time = u32::try_from(sod).ok().and_then(|s| chrono::NaiveTime::from_num_seconds_from_midnight_opt(s, 0));
match (date, time) {
(Some(d), Some(t)) => {
let y = i128::from(d.year()) + i128::from(cycles) * 400;
let y = i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX });
(y, d.month() as u8, d.day() as u8, t.hour() as u8, t.minute() as u8, t.second() as u8)
}
_ => (1970, 1, 1, 0, 0, 0), // unreachable
}
let (y, t) = designcraft_doc::time::utc_parts(t, 0);
(y, t.month() as u8, t.day() as u8, t.hour() as u8, t.minute() as u8, t.second() as u8)
}
/// A successful conversion and its name-independent diagnostic. Replayed once for each asset.
+2 -13
View File
@@ -296,19 +296,8 @@ fn core(p: &Package) -> String {
/// matching the old `civil`), then the year is clamped — never a silent reset to the epoch.
fn civil(t: i64) -> (i64, u32, u32, u32, u32, u32) {
use chrono::{Datelike, Timelike};
let (days, sod) = (t.div_euclid(86_400), t.rem_euclid(86_400));
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
// `rest + 719_163` (1970-01-01 is CE day 719_163) is always a valid CE day, and `sod` < 86_400.
let date = i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt);
let time = u32::try_from(sod).ok().and_then(|s| chrono::NaiveTime::from_num_seconds_from_midnight_opt(s, 0));
match (date, time) {
(Some(d), Some(t)) => {
let y = i128::from(d.year()) + i128::from(cycles) * 400;
let y = i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX });
(y.clamp(0, 9999), d.month(), d.day(), t.hour(), t.minute(), t.second())
}
_ => (1970, 1, 1, 0, 0, 0), // unreachable
}
let (y, t) = designcraft_doc::time::utc_parts(t, 0);
(y.clamp(0, 9999), t.month(), t.day(), t.hour(), t.minute(), t.second())
}
/// A neutral theme: text sets its own fonts and colours, so these only matter for new objects.