diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 2123586d3..4e0b74308 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -12,12 +12,14 @@ mod shape; mod text; mod util; +pub use self::metadata::{Timestamp, Timezone}; + use std::fmt::{self, Debug, Formatter}; use ecow::eco_format; use serde::{Deserialize, Serialize}; use typst_library::diag::{bail, SourceResult, StrResult}; -use typst_library::foundations::{Datetime, Smart}; +use typst_library::foundations::Smart; use typst_library::layout::{PageRanges, PagedDocument}; /// Export a document into a PDF file. @@ -186,86 +188,3 @@ pub enum PdfStandard { #[serde(rename = "a-4e")] A_4e, } - -/// A timestamp with timezone information. -#[derive(Debug, Clone, Copy)] -pub struct Timestamp { - /// The datetime of the timestamp. - pub(crate) datetime: Datetime, - /// The timezone of the timestamp. - pub(crate) timezone: Timezone, -} - -impl Timestamp { - /// Create a new timestamp with a given datetime and UTC suffix. - pub fn new_utc(datetime: Datetime) -> Self { - Self { datetime, timezone: Timezone::UTC } - } - - /// Create a new timestamp with a given datetime, and a local timezone offset. - pub fn new_local(datetime: Datetime, whole_minute_offset: i32) -> Option { - let hour_offset = (whole_minute_offset / 60).try_into().ok()?; - // Note: the `%` operator in Rust is the remainder operator, not the - // modulo operator. The remainder operator can return negative results. - // We can simply apply `abs` here because we assume the `minute_offset` - // will have the same sign as `hour_offset`. - let minute_offset = (whole_minute_offset % 60).abs().try_into().ok()?; - match (hour_offset, minute_offset) { - // Only accept valid timezone offsets with `-23 <= hours <= 23`, - // and `0 <= minutes <= 59`. - (-23..=23, 0..=59) => Some(Self { - datetime, - timezone: Timezone::Local { hour_offset, minute_offset }, - }), - _ => None, - } - } -} - -/// A timezone. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Timezone { - /// The UTC timezone. - UTC, - /// The local timezone offset from UTC. And the `minute_offset` will have - /// same sign as `hour_offset`. - Local { hour_offset: i8, minute_offset: u8 }, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_timestamp_new_local() { - let dummy_datetime = Datetime::from_ymd_hms(2024, 12, 17, 10, 10, 10).unwrap(); - let test = |whole_minute_offset, expect_timezone| { - assert_eq!( - Timestamp::new_local(dummy_datetime, whole_minute_offset) - .unwrap() - .timezone, - expect_timezone - ); - }; - - // Valid timezone offsets - test(0, Timezone::Local { hour_offset: 0, minute_offset: 0 }); - test(480, Timezone::Local { hour_offset: 8, minute_offset: 0 }); - test(-480, Timezone::Local { hour_offset: -8, minute_offset: 0 }); - test(330, Timezone::Local { hour_offset: 5, minute_offset: 30 }); - test(-210, Timezone::Local { hour_offset: -3, minute_offset: 30 }); - test(-720, Timezone::Local { hour_offset: -12, minute_offset: 0 }); // AoE - - // Corner cases - test(315, Timezone::Local { hour_offset: 5, minute_offset: 15 }); - test(-225, Timezone::Local { hour_offset: -3, minute_offset: 45 }); - test(1439, Timezone::Local { hour_offset: 23, minute_offset: 59 }); - test(-1439, Timezone::Local { hour_offset: -23, minute_offset: 59 }); - - // Invalid timezone offsets - assert!(Timestamp::new_local(dummy_datetime, 1440).is_none()); - assert!(Timestamp::new_local(dummy_datetime, -1440).is_none()); - assert!(Timestamp::new_local(dummy_datetime, i32::MAX).is_none()); - assert!(Timestamp::new_local(dummy_datetime, i32::MIN).is_none()); - } -} diff --git a/crates/typst-pdf/src/metadata.rs b/crates/typst-pdf/src/metadata.rs index 43199e6db..f6ddecd16 100644 --- a/crates/typst-pdf/src/metadata.rs +++ b/crates/typst-pdf/src/metadata.rs @@ -1,5 +1,4 @@ use crate::convert::GlobalContext; -use crate::Timezone; use ecow::EcoString; use krilla::metadata::{Metadata, TextDirection}; use typst_library::foundations::{Datetime, Smart}; @@ -99,3 +98,86 @@ fn convert_date( Some(kd) } + +/// A timestamp with timezone information. +#[derive(Debug, Clone, Copy)] +pub struct Timestamp { + /// The datetime of the timestamp. + pub(crate) datetime: Datetime, + /// The timezone of the timestamp. + pub(crate) timezone: Timezone, +} + +impl Timestamp { + /// Create a new timestamp with a given datetime and UTC suffix. + pub fn new_utc(datetime: Datetime) -> Self { + Self { datetime, timezone: Timezone::UTC } + } + + /// Create a new timestamp with a given datetime, and a local timezone offset. + pub fn new_local(datetime: Datetime, whole_minute_offset: i32) -> Option { + let hour_offset = (whole_minute_offset / 60).try_into().ok()?; + // Note: the `%` operator in Rust is the remainder operator, not the + // modulo operator. The remainder operator can return negative results. + // We can simply apply `abs` here because we assume the `minute_offset` + // will have the same sign as `hour_offset`. + let minute_offset = (whole_minute_offset % 60).abs().try_into().ok()?; + match (hour_offset, minute_offset) { + // Only accept valid timezone offsets with `-23 <= hours <= 23`, + // and `0 <= minutes <= 59`. + (-23..=23, 0..=59) => Some(Self { + datetime, + timezone: Timezone::Local { hour_offset, minute_offset }, + }), + _ => None, + } + } +} + +/// A timezone. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Timezone { + /// The UTC timezone. + UTC, + /// The local timezone offset from UTC. And the `minute_offset` will have + /// same sign as `hour_offset`. + Local { hour_offset: i8, minute_offset: u8 }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_timestamp_new_local() { + let dummy_datetime = Datetime::from_ymd_hms(2024, 12, 17, 10, 10, 10).unwrap(); + let test = |whole_minute_offset, expect_timezone| { + assert_eq!( + Timestamp::new_local(dummy_datetime, whole_minute_offset) + .unwrap() + .timezone, + expect_timezone + ); + }; + + // Valid timezone offsets + test(0, Timezone::Local { hour_offset: 0, minute_offset: 0 }); + test(480, Timezone::Local { hour_offset: 8, minute_offset: 0 }); + test(-480, Timezone::Local { hour_offset: -8, minute_offset: 0 }); + test(330, Timezone::Local { hour_offset: 5, minute_offset: 30 }); + test(-210, Timezone::Local { hour_offset: -3, minute_offset: 30 }); + test(-720, Timezone::Local { hour_offset: -12, minute_offset: 0 }); // AoE + + // Corner cases + test(315, Timezone::Local { hour_offset: 5, minute_offset: 15 }); + test(-225, Timezone::Local { hour_offset: -3, minute_offset: 45 }); + test(1439, Timezone::Local { hour_offset: 23, minute_offset: 59 }); + test(-1439, Timezone::Local { hour_offset: -23, minute_offset: 59 }); + + // Invalid timezone offsets + assert!(Timestamp::new_local(dummy_datetime, 1440).is_none()); + assert!(Timestamp::new_local(dummy_datetime, -1440).is_none()); + assert!(Timestamp::new_local(dummy_datetime, i32::MAX).is_none()); + assert!(Timestamp::new_local(dummy_datetime, i32::MIN).is_none()); + } +}