refactor(cos): consolidate duplicate civil_from_days onto pdfcraft_cos::civil_from_days (#6540)

Fixes #6466
This commit is contained in:
jonathanwxh-cell authored and GitHub committed 2026-11-09 08:32:08 +08:00
1 parent ca21968e64
commit 1f48670fce
7 files changed
+29 -79

No files matched your search

+5 -18
View File
@@ -102,25 +102,12 @@ pub fn timestamp(t: SystemTime) -> String {
format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}.{:03}Z", rem / 3600, rem % 3600 / 60, rem % 60, since.subsec_millis())
}
/// Days since 1970-01-01 → proleptic Gregorian (year, month, day).
///
/// `timestamp` clamps a pre-1970 clock to the epoch, so `days` is an unsigned count that can run
/// past `NaiveDate`'s ±262143-year range. The Gregorian calendar repeats every 400 years
/// (146_097 days), so chrono converts the day within one cycle and whole cycles go back into the
/// year (an `i128`, then saturated into `i64`), keeping chrono as the only calendar logic and
/// reproducing the old `civil_from_days` result exactly without hand-rolled day math. 1970-01-01
/// is `num_days_from_ce` 719_163.
/// Days since 1970-01-01 → proleptic Gregorian (year, month, day): `pdfcraft_engine::civil_from_days`,
/// which keeps the date past chrono's range by whole 400-year cycles. `days` is a `u64` second
/// count over 86_400, at most about 2.1e14, so it always fits an `i64`.
fn civil_from_days(days: u64) -> (i64, u32, u32) {
use chrono::Datelike;
let (cycles, rest) = (days / 146_097, days % 146_097);
// `rest + 719_163` is 719_163..865_259, always a valid CE day.
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(i64::MAX), d.month(), d.day())
}
None => (1970, 1, 1), // unreachable
}
// The fallback is unreachable: `days` is at most `u64::MAX / 86_400`, far below `i64::MAX`.
pdfcraft_engine::civil_from_days(i64::try_from(days).unwrap_or(i64::MAX))
}
fn numbered(dir: &Path, n: usize) -> PathBuf {
+1 -1
View File
@@ -30,7 +30,7 @@ pub use parser::{Lexer, is_whitespace, parse_indirect};
pub use pdfcraft_crypt::{
Algorithm, Auth, Method as CryptMethod, NewEncryption, Permissions, SecurityHandler, effective_password, pdfdoc_byte, pdfdoc_char,
};
pub use writer::{SaveOptions, pdf_date, reachable_objects, serialize, write_full, write_incremental};
pub use writer::{SaveOptions, civil_from_days, pdf_date, reachable_objects, serialize, write_full, write_incremental};
#[derive(Debug, thiserror::Error, Clone, PartialEq)]
pub enum CosError {
+13 -7
View File
@@ -919,14 +919,14 @@ pub fn pdf_date(unix_secs: i64) -> String {
format!("D:{y:04}{m:02}{d:02}{:02}{:02}{:02}Z", secs / 3600, secs / 60 % 60, secs % 60)
}
/// Days since 1970-01-01 → proleptic Gregorian (year, month, day).
/// Days since 1970-01-01 → proleptic Gregorian (year, month, day). Total for any `i64` count:
/// dates past `NaiveDate`'s ±262143-year range keep their proleptic Gregorian year instead of
/// falling back to the epoch.
///
/// A `from_timestamp` epoch fallback would change dates past `NaiveDate`'s ±262143-year range,
/// which the old hand-rolled converter formatted directly. The Gregorian calendar repeats every
/// 400 years (146_097 days), so chrono converts the day within one cycle and whole cycles go back
/// into the year (an `i128` so the extremes can't overflow), keeping chrono as the only calendar
/// logic and reproducing the old result exactly. 1970-01-01 is `num_days_from_ce` 719_163.
fn civil_from_days(days: i64) -> (i64, u32, u32) {
/// The Gregorian calendar repeats every 400 years (146_097 days), so chrono converts the day
/// within one cycle and whole cycles go back into the year (an `i128` so the extremes can't
/// overflow), keeping chrono as the only calendar logic. 1970-01-01 is `num_days_from_ce` 719_163.
pub fn civil_from_days(days: i64) -> (i64, u32, u32) {
use chrono::Datelike;
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
// `rest + 719_163` is 719_163..865_259, always a valid CE day.
@@ -1110,6 +1110,12 @@ mod tests {
// its sign, exactly as before).
assert_eq!(pdf_date(10_000_000_000_000), "D:3188570520174640Z");
assert_eq!(pdf_date(-10_000_000_000_000), "D:-3149180813061320Z");
assert_eq!(crate::civil_from_days(0), (1970, 1, 1));
assert_eq!(crate::civil_from_days(-1), (1969, 12, 31));
assert_eq!(crate::civil_from_days(11_016), (2000, 2, 29));
assert_eq!(crate::civil_from_days(19_782), (2024, 2, 29));
assert_eq!(crate::civil_from_days(i64::MAX), (25_252_734_927_768_524, 7, 27));
assert_eq!(crate::civil_from_days(i64::MIN), (-25_252_734_927_764_585, 6, 7));
}
#[test]
+1 -1
View File
@@ -86,7 +86,7 @@ pub use pdfcraft_organize::Standard;
pub use pdfcraft_organize::check_custom_key;
pub use pdfcraft_organize::view::{InitialView, Layout as InitialLayout, Magnification, Navigation};
pub use pdfcraft_cos::Algorithm;
pub use pdfcraft_cos::{Algorithm, civil_from_days};
pub use pdfcraft_create::{CONVERTIBLE, CreateError, ImageResolution, SourceKind, decode_text, source_kind};
pub use pdfcraft_edit::added::{
CHAR_SPACING_LIMIT as ADDED_CHAR_SPACING_LIMIT, paste_place as added_paste_place, paste_rect as added_paste_rect, text_rect as added_text_rect,
+2 -12
View File
@@ -349,19 +349,9 @@ impl Time {
/// year is cast exactly as the old code did (`as u32`, wrapping) — no well-formed signing time
/// is anywhere near that range, and this preserves byte-for-byte the old output everywhere.
pub fn from_unix(t: i64) -> Time {
use chrono::Datelike;
let (days, secs) = (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` is 719_163..865_259, always a valid CE day.
let (year, month, day) = 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;
// Match the old `as u32` cast: saturate i128→i64 first (the old year was i64), then wrap into u32.
(i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }) as u32, d.month(), d.day())
}
None => (1970, 1, 1), // unreachable
};
Time { year, month, day, hour: (secs / 3600) as u32, minute: (secs % 3600 / 60) as u32, second: (secs % 60) as u32 }
let (year, month, day) = pdfcraft_cos::civil_from_days(days);
Time { year: year as u32, month, day, hour: (secs / 3600) as u32, minute: (secs % 3600 / 60) as u32, second: (secs % 60) as u32 }
}
pub fn add_seconds(&self, s: i64) -> Time {
@@ -274,23 +274,10 @@ pub(super) fn utc_stamp(time: SystemTime) -> String {
format!("{y:04}-{m:02}-{d:02} {:02}:{:02} UTC", rem / 3600, rem % 3600 / 60)
}
/// Days since 1970-01-01 → proleptic Gregorian (year, month, day).
///
/// A pre-1970 time already clamps to the epoch (`unwrap_or(0)`), so `days` is an unsigned count
/// that can run past `NaiveDate`'s ±262143-year range, where the old hand-rolled converter still
/// produced a date. The Gregorian calendar repeats every 400 years (146_097 days), so chrono
/// converts the day within one cycle and whole cycles go back into the year (an `i128`, then
/// saturated into `i64`), keeping chrono as the only calendar logic and reproducing the old
/// result exactly. 1970-01-01 is `num_days_from_ce` 719_163.
/// Days since 1970-01-01 → proleptic Gregorian (year, month, day): `pdfcraft_cos::civil_from_days`,
/// which keeps the date past chrono's range by whole 400-year cycles. `days` is a `u64` second
/// count over 86_400, at most about 2.1e14, so it always fits an `i64`.
fn civil_from_days(days: u64) -> (i64, u32, u32) {
use chrono::Datelike;
let (cycles, rest) = (days / 146_097, days % 146_097);
// `rest + 719_163` is 719_163..865_259, always a valid CE day.
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(i64::MAX), d.month(), d.day())
}
None => (1970, 1, 1), // unreachable
}
// The fallback is unreachable: `days` is at most `u64::MAX / 86_400`, far below `i64::MAX`.
pdfcraft_cos::civil_from_days(i64::try_from(days).unwrap_or(i64::MAX))
}
+2 -22
View File
@@ -77,29 +77,9 @@ impl Stamp {
pub fn from_unix(secs: i64) -> Self {
let (days, rem) = (secs.div_euclid(86_400), secs.rem_euclid(86_400));
let (h, mi, s) = (rem / 3600, rem % 3600 / 60, rem % 60);
let (y, m, d) = Self::civil_from_days(days);
let (y, m, d) = pdfcraft_cos::civil_from_days(days);
// `{y:04}` rather than `%Y`, which writes a `+` before years past 9999.
Stamp { unix: secs, pdf: format!("D:{y:04}{m:02}{d:02}{h:02}{mi:02}{s:02}Z"), iso: format!("{y:04}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z") }
}
/// Days since 1970-01-01 → proleptic Gregorian (year, month, day).
///
/// Past `NaiveDate`'s ±262143-year range the old hand-rolled converter still produced a date,
/// which both stamps formatted directly; a `from_timestamp` epoch fallback would change it.
/// The Gregorian calendar repeats every 400 years (146_097 days), so chrono converts the day
/// within one cycle and whole cycles go back into the year (an `i128` so the extremes can't
/// overflow), keeping chrono as the only calendar logic. 1970-01-01 is `num_days_from_ce` 719_163.
fn civil_from_days(days: i64) -> (i64, u32, u32) {
use chrono::Datelike;
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
// `rest + 719_163` is 719_163..865_259, always a valid CE day.
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())
}
None => (1970, 1, 1), // unreachable
}
Stamp { unix: secs, pdf: pdfcraft_cos::pdf_date(secs), iso: format!("{y:04}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z") }
}
/// The same moment shifted by `minutes` (used to order threaded replies).