summaryrefslogtreecommitdiffstats
path: root/rust
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2023-12-17 14:36:26 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2023-12-17 14:36:33 +0000
commit665666d6f4213da8db57ebb480947b7caf1fe382 (patch)
tree0cac5d322dfe861a6de62b04fb916cef6dbe4510 /rust
parentReleasing debian version 3.0.0~a1-2. (diff)
downloadpendulum-665666d6f4213da8db57ebb480947b7caf1fe382.tar.xz
pendulum-665666d6f4213da8db57ebb480947b7caf1fe382.zip
Merging upstream version 3.0.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'rust')
-rw-r--r--rust/.cargo/config.toml15
-rw-r--r--rust/Cargo.lock318
-rw-r--r--rust/Cargo.toml22
-rw-r--r--rust/src/constants.rs56
-rw-r--r--rust/src/helpers.rs122
-rw-r--r--rust/src/lib.rs12
-rw-r--r--rust/src/parsing.rs905
-rw-r--r--rust/src/python/helpers.rs388
-rw-r--r--rust/src/python/mod.rs27
-rw-r--r--rust/src/python/parsing.rs117
-rw-r--r--rust/src/python/types/duration.rs59
-rw-r--r--rust/src/python/types/interval.rs46
-rw-r--r--rust/src/python/types/mod.rs7
-rw-r--r--rust/src/python/types/precise_diff.rs53
-rw-r--r--rust/src/python/types/timezone.rs52
15 files changed, 2199 insertions, 0 deletions
diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml
new file mode 100644
index 0000000..f0ba8af
--- /dev/null
+++ b/rust/.cargo/config.toml
@@ -0,0 +1,15 @@
+[build]
+rustflags = []
+
+# see https://pyo3.rs/main/building_and_distribution.html#macos
+[target.x86_64-apple-darwin]
+rustflags = [
+ "-C", "link-arg=-undefined",
+ "-C", "link-arg=dynamic_lookup",
+]
+
+[target.aarch64-apple-darwin]
+rustflags = [
+ "-C", "link-arg=-undefined",
+ "-C", "link-arg=dynamic_lookup",
+]
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
new file mode 100644
index 0000000..bcfe643
--- /dev/null
+++ b/rust/Cargo.lock
@@ -0,0 +1,318 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "_pendulum"
+version = "3.0.0-beta-1"
+dependencies = [
+ "mimalloc",
+ "pyo3",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "indoc"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
+
+[[package]]
+name = "libc"
+version = "0.2.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "libmimalloc-sys"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3979b5c37ece694f1f5e51e7ecc871fdb0f517ed04ee45f88d15d6d553cb9664"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mimalloc"
+version = "0.1.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa01922b5ea280a911e323e4d2fd24b7fe5cc4042e0d2cda3c40775cdc4bdc9c"
+dependencies = [
+ "libmimalloc-sys",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb88ae05f306b4bfcde40ac4a51dc0b05936a9207a4b75b798c7729c4258a59"
+dependencies = [
+ "cfg-if",
+ "indoc",
+ "libc",
+ "memoffset",
+ "parking_lot",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+ "unindent",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "554db24f0b3c180a9c0b1268f91287ab3f17c162e15b54caaae5a6b3773396b0"
+dependencies = [
+ "once_cell",
+ "python3-dll-a",
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "922ede8759e8600ad4da3195ae41259654b9c55da4f7eec84a0ccc7d067a70a4"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a5caec6a1dd355964a841fcbeeb1b89fe4146c87295573f94228911af3cc5a2"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0b78ccbb160db1556cdb6fd96c50334c5d4ec44dc5e0a968d0a1208fa0efa8b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "python3-dll-a"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f07cd4412be8fa09a721d40007c483981bbe072cd6a21f2e83e04ec8f8343f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae9980cab1db3fceee2f6c6f643d5d8de2997c58ee8d25fb0cc8a9e9e7348e5"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unindent"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c"
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
new file mode 100644
index 0000000..0d76a89
--- /dev/null
+++ b/rust/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "_pendulum"
+version = "3.0.0"
+edition = "2021"
+
+[lib]
+name = "_pendulum"
+crate-type = ["cdylib", "rlib"]
+
+[profile.release]
+lto = "fat"
+codegen-units = 1
+strip = true
+overflow-checks = false
+
+[dependencies]
+pyo3 = { version = "0.19.0", features = ["extension-module", "generate-import-lib"] }
+mimalloc = { version = "0.1.39", optional = true, default-features = false }
+
+[features]
+extension-module = ["pyo3/extension-module"]
+default = ["mimalloc"]
diff --git a/rust/src/constants.rs b/rust/src/constants.rs
new file mode 100644
index 0000000..3fea9c0
--- /dev/null
+++ b/rust/src/constants.rs
@@ -0,0 +1,56 @@
+pub const EPOCH_YEAR: u32 = 1970;
+
+pub const DAYS_PER_N_YEAR: u32 = 365;
+pub const DAYS_PER_L_YEAR: u32 = 366;
+
+pub const SECS_PER_MIN: u32 = 60;
+pub const SECS_PER_HOUR: u32 = SECS_PER_MIN * 60;
+pub const SECS_PER_DAY: u32 = SECS_PER_HOUR * 24;
+
+// 400-year chunks always have 146097 days (20871 weeks).
+pub const DAYS_PER_400_YEARS: u32 = 146_097;
+pub const SECS_PER_400_YEARS: u64 = DAYS_PER_400_YEARS as u64 * SECS_PER_DAY as u64;
+
+// The number of seconds in an aligned 100-year chunk, for those that
+// do not begin with a leap year and those that do respectively.
+pub const SECS_PER_100_YEARS: [u64; 2] = [
+ (76 * DAYS_PER_N_YEAR as u64 + 24 * DAYS_PER_L_YEAR as u64) * SECS_PER_DAY as u64,
+ (75 * DAYS_PER_N_YEAR as u64 + 25 * DAYS_PER_L_YEAR as u64) * SECS_PER_DAY as u64,
+];
+
+// The number of seconds in an aligned 4-year chunk, for those that
+// do not begin with a leap year and those that do respectively.
+#[allow(clippy::erasing_op)]
+pub const SECS_PER_4_YEARS: [u32; 2] = [
+ (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY,
+ (3 * DAYS_PER_N_YEAR + DAYS_PER_L_YEAR) * SECS_PER_DAY,
+];
+
+// The number of seconds in non-leap and leap years respectively.
+pub const SECS_PER_YEAR: [u32; 2] = [
+ DAYS_PER_N_YEAR * SECS_PER_DAY,
+ DAYS_PER_L_YEAR * SECS_PER_DAY,
+];
+
+// The month lengths in non-leap and leap years respectively.
+pub const DAYS_PER_MONTHS: [[i32; 13]; 2] = [
+ [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
+ [-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
+];
+
+// The day offsets of the beginning of each (1-based) month in non-leap
+// and leap years respectively.
+// For example, in a leap year there are 335 days before December.
+pub const MONTHS_OFFSETS: [[i32; 14]; 2] = [
+ [
+ -1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365,
+ ],
+ [
+ -1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366,
+ ],
+];
+
+pub const DAY_OF_WEEK_TABLE: [u32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
+
+pub const TM_JANUARY: usize = 0;
+pub const TM_DECEMBER: usize = 11;
diff --git a/rust/src/helpers.rs b/rust/src/helpers.rs
new file mode 100644
index 0000000..364075a
--- /dev/null
+++ b/rust/src/helpers.rs
@@ -0,0 +1,122 @@
+use crate::constants::{
+ DAYS_PER_L_YEAR, DAYS_PER_N_YEAR, DAY_OF_WEEK_TABLE, EPOCH_YEAR, MONTHS_OFFSETS,
+ SECS_PER_100_YEARS, SECS_PER_400_YEARS, SECS_PER_4_YEARS, SECS_PER_DAY, SECS_PER_HOUR,
+ SECS_PER_MIN, SECS_PER_YEAR, TM_DECEMBER, TM_JANUARY,
+};
+
+fn p(year: i32) -> i32 {
+ year + year / 4 - year / 100 + year / 400
+}
+
+pub fn is_leap(year: i32) -> bool {
+ year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
+}
+
+pub fn is_long_year(year: i32) -> bool {
+ (p(year) % 7 == 4) || (p(year - 1) % 7 == 3)
+}
+
+pub fn days_in_year(year: i32) -> u32 {
+ if is_leap(year) {
+ return DAYS_PER_L_YEAR;
+ }
+
+ DAYS_PER_N_YEAR
+}
+
+pub fn week_day(year: i32, month: u32, day: u32) -> u32 {
+ let y: i32 = year - i32::from(month < 3);
+
+ let w: i32 = (p(y) + DAY_OF_WEEK_TABLE[(month - 1) as usize] as i32 + day as i32) % 7;
+
+ if w == 0 {
+ return 7;
+ }
+
+ w.unsigned_abs()
+}
+
+pub fn day_number(year: i32, month: u8, day: u8) -> i32 {
+ let m = i32::from((month + 9) % 12);
+ let y = year - m / 10;
+
+ 365 * y + y / 4 - y / 100 + y / 400 + (m * 306 + 5) / 10 + (i32::from(day) - 1)
+}
+
+pub fn local_time(
+ unix_time: f64,
+ utc_offset: isize,
+ microsecond: usize,
+) -> (usize, usize, usize, usize, usize, usize, usize) {
+ let mut year: usize = EPOCH_YEAR as usize;
+ let mut seconds: isize = unix_time.floor() as isize;
+
+ // Shift to a base year that is 400-year aligned.
+ if seconds >= 0 {
+ seconds -= (10957 * SECS_PER_DAY as usize) as isize;
+ year += 30; // == 2000
+ } else {
+ seconds += ((146_097 - 10957) * SECS_PER_DAY as usize) as isize;
+ year -= 370; // == 1600
+ }
+
+ seconds += utc_offset;
+
+ // Handle years in chunks of 400/100/4/1
+ year += 400 * (seconds / SECS_PER_400_YEARS as isize) as usize;
+ seconds %= SECS_PER_400_YEARS as isize;
+ if seconds < 0 {
+ seconds += SECS_PER_400_YEARS as isize;
+ year -= 400;
+ }
+
+ let mut leap_year = 1; // 4-century aligned
+ let mut sec_per_100years = SECS_PER_100_YEARS[leap_year] as isize;
+
+ while seconds >= sec_per_100years {
+ seconds -= sec_per_100years;
+ year += 100;
+ leap_year = 0; // 1-century, non 4-century aligned
+ sec_per_100years = SECS_PER_100_YEARS[leap_year] as isize;
+ }
+
+ let mut sec_per_4years = SECS_PER_4_YEARS[leap_year] as isize;
+ while seconds >= sec_per_4years {
+ seconds -= sec_per_4years;
+ year += 4;
+ leap_year = 1; // 4-year, non century aligned
+ sec_per_4years = SECS_PER_4_YEARS[leap_year] as isize;
+ }
+
+ let mut sec_per_year = SECS_PER_YEAR[leap_year] as isize;
+ while seconds >= sec_per_year {
+ seconds -= sec_per_year;
+ year += 1;
+ leap_year = 0; // non 4-year aligned
+ sec_per_year = SECS_PER_YEAR[leap_year] as isize;
+ }
+
+ // Handle months and days
+ let mut month = TM_DECEMBER + 1;
+ let mut day: usize = (seconds / (SECS_PER_DAY as isize) + 1) as usize;
+ seconds %= SECS_PER_DAY as isize;
+
+ let mut month_offset: usize;
+ while month != (TM_JANUARY + 1) {
+ month_offset = MONTHS_OFFSETS[leap_year][month] as usize;
+ if day > month_offset {
+ day -= month_offset;
+ break;
+ }
+
+ month -= 1;
+ }
+
+ // Handle hours, minutes and seconds
+ let hour: usize = (seconds / SECS_PER_HOUR as isize) as usize;
+ seconds %= SECS_PER_HOUR as isize;
+ let minute: usize = (seconds / SECS_PER_MIN as isize) as usize;
+ let second: usize = (seconds % SECS_PER_MIN as isize) as usize;
+
+ (year, month, day, hour, minute, second, microsecond)
+}
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
new file mode 100644
index 0000000..bd0f1f6
--- /dev/null
+++ b/rust/src/lib.rs
@@ -0,0 +1,12 @@
+extern crate core;
+
+#[cfg(feature = "mimalloc")]
+#[global_allocator]
+static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
+
+mod constants;
+mod helpers;
+mod parsing;
+mod python;
+
+pub use python::_pendulum;
diff --git a/rust/src/parsing.rs b/rust/src/parsing.rs
new file mode 100644
index 0000000..757a3e3
--- /dev/null
+++ b/rust/src/parsing.rs
@@ -0,0 +1,905 @@
+use core::str;
+use std::{fmt, str::CharIndices};
+
+use crate::{
+ constants::MONTHS_OFFSETS,
+ helpers::{days_in_year, is_leap, is_long_year, week_day},
+};
+
+#[derive(Debug, Clone)]
+pub struct ParseError {
+ index: usize,
+ message: String,
+}
+
+impl fmt::Display for ParseError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{} (Position: {})", self.message, self.index)
+ }
+}
+
+pub struct ParsedDateTime {
+ pub year: u32,
+ pub month: u32,
+ pub day: u32,
+ pub hour: u32,
+ pub minute: u32,
+ pub second: u32,
+ pub microsecond: u32,
+ pub offset: Option<i32>,
+ pub has_offset: bool,
+ pub tzname: Option<String>,
+ pub has_date: bool,
+ pub has_time: bool,
+ pub extended_date_format: bool,
+ pub time_is_midnight: bool,
+}
+
+impl ParsedDateTime {
+ pub fn new() -> ParsedDateTime {
+ ParsedDateTime {
+ year: 0,
+ month: 1,
+ day: 1,
+ hour: 0,
+ minute: 0,
+ second: 0,
+ microsecond: 0,
+ offset: None,
+ has_offset: false,
+ tzname: None,
+ has_date: false,
+ has_time: false,
+ extended_date_format: false,
+ time_is_midnight: false,
+ }
+ }
+}
+
+pub struct ParsedDuration {
+ pub years: u32,
+ pub months: u32,
+ pub weeks: u32,
+ pub days: u32,
+ pub hours: u32,
+ pub minutes: u32,
+ pub seconds: u32,
+ pub microseconds: u32,
+}
+
+impl ParsedDuration {
+ pub fn new() -> ParsedDuration {
+ ParsedDuration {
+ years: 0,
+ months: 0,
+ weeks: 0,
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ seconds: 0,
+ microseconds: 0,
+ }
+ }
+}
+
+pub struct Parsed {
+ pub datetime: Option<ParsedDateTime>,
+ pub duration: Option<ParsedDuration>,
+ pub second_datetime: Option<ParsedDateTime>,
+}
+
+impl Parsed {
+ pub fn new() -> Parsed {
+ Parsed {
+ datetime: None,
+ duration: None,
+ second_datetime: None,
+ }
+ }
+}
+
+pub struct Parser<'a> {
+ /// Input to parse.
+ src: &'a str,
+ /// Iterator used for getting characters from `src`.
+ chars: CharIndices<'a>,
+ /// Current byte offset into `src`.
+ idx: usize,
+ /// Current character
+ current: char,
+}
+
+impl<'a> Parser<'a> {
+ /// Creates a new parser from a &str.
+ pub fn new(input: &'a str) -> Parser<'a> {
+ let mut p = Parser {
+ src: input,
+ chars: input.char_indices(),
+ idx: 0,
+ current: '\0',
+ };
+ p.inc();
+ p
+ }
+
+ /// Increments the parser if the end of the input has not been reached.
+ /// Returns whether or not it was able to advance.
+ fn inc(&mut self) -> Option<char> {
+ if let Some((i, ch)) = self.chars.next() {
+ self.idx = i;
+ self.current = ch;
+ Some(ch)
+ } else {
+ self.idx = self.src.len();
+ self.current = '\0';
+ None
+ }
+ }
+
+ fn parse_error(&mut self, message: String) -> ParseError {
+ ParseError {
+ index: self.idx,
+ message,
+ }
+ }
+
+ fn unexpected_character_error(
+ &mut self,
+ field_name: &str,
+ expected_character_count: usize,
+ ) -> ParseError {
+ if self.end() {
+ return self.parse_error(format!(
+ "Unexpected end of string while parsing {}. Expected {} more character{}.",
+ field_name,
+ expected_character_count,
+ if expected_character_count == 1 {
+ ""
+ } else {
+ "s"
+ }
+ ));
+ }
+
+ self.parse_error(format!(
+ "Invalid character while parsing {}: {}.",
+ field_name, self.current,
+ ))
+ }
+
+ /// Returns true if the parser has reached the end of the input.
+ fn end(&self) -> bool {
+ self.idx >= self.src.len()
+ }
+
+ fn parse_integer(&mut self, length: usize, field_name: &str) -> Result<u32, ParseError> {
+ let mut value: u32 = 0;
+
+ for i in 0..length {
+ if self.end() {
+ return Err(self.parse_error(format!(
+ "Unexpected end of string while parsing \"{}\". Expected {} more character{}",
+ field_name,
+ length - i,
+ if (length - i) != 1 { "s" } else { "" }
+ )));
+ }
+
+ if let Some(digit) = self.current.to_digit(10) {
+ value = 10 * value + digit;
+ self.inc();
+ } else {
+ return Err(self.unexpected_character_error(field_name, length - i));
+ }
+ }
+
+ Ok(value)
+ }
+
+ pub fn parse(&mut self) -> Result<Parsed, ParseError> {
+ let mut parsed = Parsed::new();
+
+ if self.current == 'P' {
+ // Duration (and possibly time interval)
+ self.parse_duration(&mut parsed)?;
+ } else {
+ self.parse_datetime(&mut parsed)?;
+ }
+
+ Ok(parsed)
+ }
+
+ fn parse_datetime(&mut self, parsed: &mut Parsed) -> Result<(), ParseError> {
+ let mut datetime = ParsedDateTime::new();
+
+ if self.current == 'T' {
+ self.parse_time(&mut datetime, false)?;
+
+ if !self.end() {
+ return Err(self.parse_error("Unconverted data remains".to_string()));
+ }
+
+ match &parsed.datetime {
+ Some(_) => {
+ parsed.second_datetime = Some(datetime);
+ }
+ None => match &parsed.duration {
+ Some(_) => {
+ parsed.second_datetime = Some(datetime);
+ }
+ None => {
+ parsed.datetime = Some(datetime);
+ }
+ },
+ }
+
+ return Ok(());
+ }
+
+ datetime.year = self.parse_integer(2, "year")?;
+
+ if self.current == ':' {
+ // Time in extended format
+ datetime.hour = datetime.year;
+ datetime.year = 0;
+ datetime.extended_date_format = true;
+ self.parse_time(&mut datetime, true)?;
+
+ if !self.end() {
+ return Err(self.parse_error("Unconverted data remains".to_string()));
+ }
+
+ match &parsed.datetime {
+ Some(_) => {
+ parsed.second_datetime = Some(datetime);
+ }
+ None => match &parsed.duration {
+ Some(_) => {
+ parsed.second_datetime = Some(datetime);
+ }
+ None => {
+ parsed.datetime = Some(datetime);
+ }
+ },
+ }
+
+ return Ok(());
+ }
+
+ datetime.has_date = true;
+ datetime.year = datetime.year * 100 + self.parse_integer(2, "year")?;
+
+ if self.current == '-' {
+ self.inc();
+ datetime.extended_date_format = true;
+
+ if self.current == 'W' {
+ // ISO week and day in extended format (i.e. Www-D)
+ self.inc();
+
+ let iso_week = self.parse_integer(2, "iso week")?;
+ let mut iso_day: u32 = 1;
+
+ if !self.end() && self.current != ' ' && self.current != 'T' {
+ // Optional day
+ if self.current != '-' {
+ return Err(self.parse_error(format!(
+ "Invalid character \"{}\" while parsing {}",
+ self.current, "date separator"
+ )));
+ }
+
+ self.inc();
+
+ iso_day = self.parse_integer(1, "iso day")?;
+ }
+
+ let (year, month, day) = self.iso_to_ymd(datetime.year, iso_week, iso_day)?;
+
+ datetime.year = year;
+ datetime.month = month;
+ datetime.day = day;
+ } else {
+ /*
+ Month and day in extended format (MM-DD) or ordinal date (DDD)
+ We'll assume first that the next part is a month and adjust if not.
+ */
+ datetime.month = self.parse_integer(2, "month")?;
+
+ if !self.end() && self.current != ' ' && self.current != 'T' {
+ if self.current == '-' {
+ // Optional day
+ self.inc();
+ datetime.day = self.parse_integer(2, "day")?;
+ } else {
+ // Ordinal day
+ let ordinal_day =
+ (datetime.month * 10 + self.parse_integer(1, "ordinal day")?) as i32;
+
+ let (year, month, day) =
+ self.ordinal_to_ymd(datetime.year, ordinal_day, false)?;
+
+ datetime.year = year;
+ datetime.month = month;
+ datetime.day = day;
+ }
+ } else {
+ datetime.day = 1;
+ }
+ }
+ } else if self.current == 'W' {
+ // Compact ISO week and day (WwwD)
+ self.inc();
+
+ let iso_week = self.parse_integer(2, "iso week")?;
+ let mut iso_day: u32 = 1;
+
+ if !self.end() && self.current != ' ' && self.current != 'T' {
+ iso_day = self.parse_integer(1, "iso day")?;
+ }
+
+ match self.iso_to_ymd(datetime.year, iso_week, iso_day) {
+ Ok((year, month, day)) => {
+ datetime.year = year;
+ datetime.month = month;
+ datetime.day = day;
+ }
+ Err(error) => return Err(error),
+ }
+ } else {
+ /*
+ Month and day in compact format (MMDD) or ordinal date (DDD)
+ We'll assume first that the next part is a month and adjust if not.
+ */
+ datetime.month = self.parse_integer(2, "month")?;
+ let mut ordinal_day = self.parse_integer(1, "ordinal day")? as i32;
+
+ if self.end() || self.current == ' ' || self.current == 'T' {
+ // Ordinal day
+ ordinal_day += datetime.month as i32 * 10;
+
+ let (year, month, day) = self.ordinal_to_ymd(datetime.year, ordinal_day, false)?;
+
+ datetime.year = year;
+ datetime.month = month;
+ datetime.day = day;
+ } else {
+ // Day
+ datetime.day = ordinal_day as u32 * 10 + self.parse_integer(1, "day")?;
+ }
+ }
+
+ if !self.end() {
+ self.parse_time(&mut datetime, false)?;
+ }
+
+ if !self.end() {
+ if self.current == '/' && parsed.datetime.is_none() && parsed.duration.is_none() {
+ // Interval
+ parsed.datetime = Some(datetime);
+
+ self.inc();
+
+ if self.current == 'P' {
+ // Duration
+ self.parse_duration(parsed)?;
+ } else {
+ self.parse_datetime(parsed)?;
+ }
+
+ return Ok(());
+ }
+
+ return Err(self.parse_error("Unconverted data remains".to_string()));
+ }
+
+ match &parsed.datetime {
+ Some(_) => {
+ parsed.second_datetime = Some(datetime);
+ }
+ None => match &parsed.duration {
+ Some(_) => {
+ parsed.second_datetime = Some(datetime);
+ }
+ None => {
+ parsed.datetime = Some(datetime);
+ }
+ },
+ }
+
+ Ok(())
+ }
+
+ fn parse_time(
+ &mut self,
+ datetime: &mut ParsedDateTime,
+ skip_hour: bool,
+ ) -> Result<(), ParseError> {
+ // TODO: Add support for decimal units
+
+ // Date/Time separator
+ if self.current != 'T' && self.current != ' ' && !skip_hour {
+ return Err(self.parse_error(format!(
+ "Invalid character \"{}\" while parsing {}",
+ self.current, "date and time separator (\"T\" or \" \")"
+ )));
+ }
+
+ datetime.has_time = true;
+
+ if !skip_hour {
+ self.inc();
+
+ // Hour
+ datetime.hour = self.parse_integer(2, "hour")?;
+ }
+
+ if !self.end() && self.current != 'Z' && self.current != '+' && self.current != '-' {
+ // Optional minute and second
+ if self.current == ':' {
+ // Minute and second in extended format (mm:ss)
+ self.inc();
+
+ // Minute
+ datetime.minute = self.parse_integer(2, "minute")?;
+
+ if !self.end() && self.current != 'Z' && self.current != '+' && self.current != '-'
+ {
+ // Optional second
+ if self.current != ':' {
+ return Err(self.parse_error(format!(
+ "Invalid character \"{}\" while parsing {}",
+ self.current, "time separator (\":\")"
+ )));
+ }
+
+ self.inc();
+
+ // Second
+ datetime.second = self.parse_integer(2, "second")?;
+
+ if self.current == '.' || self.current == ',' {
+ // Optional fractional second
+ self.inc();
+
+ datetime.microsecond = 0;
+ let mut i: u8 = 0;
+
+ while i < 6 {
+ if let Some(digit) = self.current.to_digit(10) {
+ datetime.microsecond = datetime.microsecond * 10 + digit;
+ } else if i == 0 {
+ // One digit minimum is required
+ return Err(self.unexpected_character_error("subsecond", 1));
+ } else {
+ break;
+ }
+
+ self.inc();
+ i += 1;
+ }
+
+ // Drop extraneous digits
+ while self.current.is_ascii_digit() {
+ self.inc();
+ }
+
+ // Expand missing microsecond
+ while i < 6 {
+ datetime.microsecond *= 10;
+ i += 1;
+ }
+ }
+
+ if !datetime.extended_date_format {
+ return Err(self.parse_error("Cannot combine \"basic\" date format with \"extended\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`).".to_string()));
+ }
+ }
+ } else {
+ // Minute and second in compact format (mmss)
+
+ // Minute
+ datetime.minute = self.parse_integer(2, "minute")?;
+
+ if !self.end() && self.current != 'Z' && self.current != '+' && self.current != '-'
+ {
+ // Optional second
+
+ datetime.second = self.parse_integer(2, "second")?;
+
+ if self.current == '.' || self.current == ',' {
+ // Optional fractional second
+ self.inc();
+
+ datetime.microsecond = 0;
+ let mut i: u8 = 0;
+
+ while i < 6 {
+ if let Some(digit) = self.current.to_digit(10) {
+ datetime.microsecond = datetime.microsecond * 10 + digit;
+ } else if i == 0 {
+ // One digit minimum is required
+ return Err(self.unexpected_character_error("subsecond", 1));
+ } else {
+ break;
+ }
+
+ self.inc();
+ i += 1;
+ }
+
+ // Drop extraneous digits
+ while self.current.is_ascii_digit() {
+ self.inc();
+ }
+
+ // Expand missing microsecond
+ while i < 6 {
+ datetime.microsecond *= 10;
+ i += 1;
+ }
+ }
+ }
+
+ if datetime.extended_date_format {
+ return Err(self.parse_error("Cannot combine \"extended\" date format with \"basic\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`).".to_string()));
+ }
+ }
+ }
+
+ if datetime.hour == 24
+ && datetime.minute == 0
+ && datetime.second == 0
+ && datetime.microsecond == 0
+ {
+ // Special case for 24:00:00, which is valid for ISO 8601.
+ // This is equivalent to 00:00:00 the next day.
+ // We will store the information for now.
+ datetime.time_is_midnight = true;
+ }
+
+ if self.current == 'Z' {
+ // UTC
+ datetime.offset = Some(0);
+ datetime.tzname = Some("UTC".to_string());
+ self.inc();
+ } else if matches!(self.current, '+' | '-') {
+ // Optional timezone offset
+ let tzsign = if self.current == '+' { 1 } else { -1 };
+ self.inc();
+ // Offset hour
+ let tzhour = self.parse_integer(2, "timezone hour")? as i32;
+ if self.current == ':' {
+ // Optional separator
+ self.inc();
+ }
+ let mut tzminute = if self.end() {
+ 0
+ } else {
+ // Optional minute
+ self.parse_integer(2, "timezone minute")? as i32
+ };
+ tzminute += tzhour * 60;
+ tzminute *= tzsign;
+ if tzminute > 24 * 60 {
+ return Err(self.parse_error("Timezone offset is too large".to_string()));
+ }
+ datetime.offset = Some(tzminute * 60);
+ }
+
+ Ok(())
+ }
+
+ fn parse_duration(&mut self, parsed: &mut Parsed) -> Result<(), ParseError> {
+ // Removing P operator
+ self.inc();
+
+ let mut duration: ParsedDuration = ParsedDuration::new();
+ let mut got_t: bool = false;
+ let mut last_had_fraction = false;
+
+ loop {
+ match self.current {
+ 'T' => {
+ if got_t {
+ return Err(
+ self.parse_error("Repeated time declaration in duration".to_string())
+ );
+ }
+
+ got_t = true;
+ }
+ _c => {
+ let (value, op_fraction) = self.parse_duration_number_frac()?;
+ if last_had_fraction {
+ return Err(self.parse_error("Invalid duration fraction".to_string()));
+ }
+
+ if op_fraction.is_some() {
+ last_had_fraction = true;
+ }
+
+ if got_t {
+ match self.current {
+ 'H' => {
+ if duration.minutes != 0
+ || duration.seconds != 0
+ || duration.microseconds != 0
+ {
+ return Err(
+ self.parse_error("Duration units out of order".to_string())
+ );
+ }
+
+ duration.hours += value;
+
+ if let Some(fraction) = op_fraction {
+ let extra_minutes = fraction * 60_f64;
+ let extra_full_minutes: f64 = extra_minutes.trunc();
+ duration.minutes += extra_full_minutes as u32;
+ let extra_seconds =
+ ((extra_minutes - extra_full_minutes) * 60.0).round();
+ let extra_full_seconds = extra_seconds.trunc();
+ duration.seconds += extra_full_seconds as u32;
+ let micro_extra = ((extra_seconds - extra_full_seconds)
+ * 1_000_000.0)
+ .round()
+ as u32;
+ duration.microseconds += micro_extra;
+ }
+ }
+ 'M' => {
+ if duration.seconds != 0 || duration.microseconds != 0 {
+ return Err(
+ self.parse_error("Duration units out of order".to_string())
+ );
+ }
+
+ duration.minutes += value;
+
+ if let Some(fraction) = op_fraction {
+ let extra_seconds = fraction * 60_f64;
+ let extra_full_seconds = extra_seconds.trunc();
+ duration.seconds += extra_full_seconds as u32;
+ let micro_extra = ((extra_seconds - extra_full_seconds)
+ * 1_000_000.0)
+ .round()
+ as u32;
+ duration.microseconds += micro_extra;
+ }
+ }
+ 'S' => {
+ duration.seconds = value;
+
+ if let Some(fraction) = op_fraction {
+ duration.microseconds +=
+ (fraction * 1_000_000.0).round() as u32;
+ }
+ }
+ _ => {
+ return Err(
+ self.parse_error("Invalid duration time unit".to_string())
+ )
+ }
+ }
+ } else {
+ match self.current {
+ 'Y' => {
+ if last_had_fraction {
+ return Err(self.parse_error(
+ "Fractional years in duration are not supported"
+ .to_string(),
+ ));
+ }
+
+ if duration.months != 0 || duration.days != 0 {
+ return Err(
+ self.parse_error("Duration units out of order".to_string())
+ );
+ }
+
+ duration.years = value;
+ }
+ 'M' => {
+ if last_had_fraction {
+ return Err(self.parse_error(
+ "Fractional months in duration are not supported"
+ .to_string(),
+ ));
+ }
+
+ if duration.days != 0 {
+ return Err(
+ self.parse_error("Duration units out of order".to_string())
+ );
+ }
+
+ duration.months = value;
+ }
+ 'W' => {
+ if duration.years != 0 || duration.months != 0 {
+ return Err(self.parse_error(
+ "Basic format durations cannot have weeks".to_string(),
+ ));
+ }
+
+ duration.weeks = value;
+
+ if let Some(fraction) = op_fraction {
+ let extra_days = fraction * 7_f64;
+ let extra_full_days = extra_days.trunc();
+ duration.days += extra_full_days as u32;
+ let extra_hours = (extra_days - extra_full_days) * 24.0;
+ let extra_full_hours = extra_hours.trunc();
+ duration.hours += extra_full_hours as u32;
+ let extra_minutes =
+ ((extra_hours - extra_full_hours) * 60.0).round();
+ let extra_full_minutes: f64 = extra_minutes.trunc();
+ duration.minutes += extra_full_minutes as u32;
+ let extra_seconds =
+ ((extra_minutes - extra_full_minutes) * 60.0).round();
+ let extra_full_seconds = extra_seconds.trunc();
+ duration.seconds += extra_full_seconds as u32;
+ let micro_extra = ((extra_seconds - extra_full_seconds)
+ * 1_000_000.0)
+ .round()
+ as u32;
+ duration.microseconds += micro_extra;
+ }
+ }
+ 'D' => {
+ if duration.weeks != 0 {
+ return Err(self.parse_error(
+ "Week format durations cannot have days".to_string(),
+ ));
+ }
+
+ duration.days += value;
+ if let Some(fraction) = op_fraction {
+ let extra_hours = fraction * 24.0;
+ let extra_full_hours = extra_hours.trunc();
+ duration.hours += extra_full_hours as u32;
+ let extra_minutes =
+ ((extra_hours - extra_full_hours) * 60.0).round();
+ let extra_full_minutes: f64 = extra_minutes.trunc();
+ duration.minutes += extra_full_minutes as u32;
+ let extra_seconds =
+ ((extra_minutes - extra_full_minutes) * 60.0).round();
+ let extra_full_seconds = extra_seconds.trunc();
+ duration.seconds += extra_full_seconds as u32;
+ let micro_extra = ((extra_seconds - extra_full_seconds)
+ * 1_000_000.0)
+ .round()
+ as u32;
+ duration.microseconds += micro_extra;
+ }
+ }
+ _ => {
+ return Err(
+ self.parse_error("Invalid duration time unit".to_string())
+ )
+ }
+ }
+ }
+ }
+ }
+ self.inc();
+
+ if self.end() {
+ break;
+ }
+ }
+
+ parsed.duration = Some(duration);
+
+ Ok(())
+ }
+
+ fn parse_duration_number_frac(&mut self) -> Result<(u32, Option<f64>), ParseError> {
+ let value = self.parse_duration_number()?;
+ let fraction = matches!(self.current, '.' | ',').then(|| {
+ let mut decimal = 0_f64;
+ let mut denominator = 1_f64;
+
+ while let Some(digit) = self.inc().and_then(|ch| ch.to_digit(10)) {
+ decimal *= 10.0;
+ decimal += f64::from(digit);
+ denominator *= 10.0;
+ }
+
+ decimal / denominator
+ });
+
+ Ok((value, fraction))
+ }
+
+ fn parse_duration_number(&mut self) -> Result<u32, ParseError> {
+ let Some(mut value) = self.current.to_digit(10) else {
+ return Err(self.parse_error("Invalid number in duration".to_string()));
+ };
+
+ while let Some(digit) = self.inc().and_then(|ch| ch.to_digit(10)) {
+ value *= 10;
+ value += digit;
+ }
+
+ Ok(value)
+ }
+
+ fn iso_to_ymd(
+ &mut self,
+ iso_year: u32,
+ iso_week: u32,
+ iso_day: u32,
+ ) -> Result<(u32, u32, u32), ParseError> {
+ if iso_week > 53 || iso_week > 52 && !is_long_year(iso_year as i32) {
+ return Err(ParseError {
+ index: self.idx,
+ message: format!(
+ "Invalid ISO date: week {iso_week} out of range for year {iso_year}"
+ ),
+ });
+ }
+
+ if iso_day > 7 {
+ return Err(ParseError {
+ index: self.idx,
+ message: "Invalid ISO date: week day is invalid".to_string(),
+ });
+ }
+
+ let ordinal: i32 =
+ iso_week as i32 * 7 + iso_day as i32 - (week_day(iso_year as i32, 1, 4) as i32 + 3);
+
+ self.ordinal_to_ymd(iso_year, ordinal, true)
+ }
+
+ fn ordinal_to_ymd(
+ &mut self,
+ year: u32,
+ ordinal: i32,
+ allow_out_of_bounds: bool,
+ ) -> Result<(u32, u32, u32), ParseError> {
+ let mut ord: i32 = ordinal;
+ let mut y: u32 = year;
+ let mut leap: usize = usize::from(is_leap(y as i32));
+
+ if ord < 1 {
+ if !allow_out_of_bounds {
+ return Err(self.parse_error(format!(
+ "Invalid ordinal day: {ordinal} is too small for year {year}"
+ )));
+ }
+ // Previous year
+ ord += days_in_year((year - 1) as i32) as i32;
+ y -= 1;
+ leap = usize::from(is_leap(y as i32));
+ }
+
+ if ord > days_in_year(y as i32) as i32 {
+ if !allow_out_of_bounds {
+ return Err(self.parse_error(format!(
+ "Invalid ordinal day: {ordinal} is too large for year {year}"
+ )));
+ }
+
+ // Next year
+ ord -= days_in_year(y as i32) as i32;
+ y += 1;
+ leap = usize::from(is_leap(y as i32));
+ }
+
+ for i in 1..14 {
+ if ord < MONTHS_OFFSETS[leap][i] {
+ let day = ord as u32 - MONTHS_OFFSETS[leap][i - 1] as u32;
+ let month = (i - 1) as u32;
+
+ return Ok((y, month, day));
+ }
+ }
+
+ Err(self.parse_error(format!(
+ "Invalid ordinal day: {ordinal} is too large for year {year}"
+ )))
+ }
+}
diff --git a/rust/src/python/helpers.rs b/rust/src/python/helpers.rs
new file mode 100644
index 0000000..4a53e59
--- /dev/null
+++ b/rust/src/python/helpers.rs
@@ -0,0 +1,388 @@
+use std::cmp::Ordering;
+
+use pyo3::{
+ intern,
+ prelude::*,
+ types::{PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyString, PyTimeAccess},
+ PyTypeInfo,
+};
+
+use crate::{
+ constants::{DAYS_PER_MONTHS, SECS_PER_DAY, SECS_PER_HOUR, SECS_PER_MIN},
+ helpers,
+};
+
+use crate::python::types::PreciseDiff;
+
+struct DateTimeInfo<'py> {
+ pub year: i32,
+ pub month: i32,
+ pub day: i32,
+ pub hour: i32,
+ pub minute: i32,
+ pub second: i32,
+ pub microsecond: i32,
+ pub total_seconds: i32,
+ pub offset: i32,
+ pub tz: &'py str,
+ pub is_datetime: bool,
+}
+
+impl PartialEq for DateTimeInfo<'_> {
+ fn eq(&self, other: &Self) -> bool {
+ (
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ )
+ .eq(&(
+ other.year,
+ other.month,
+ other.day,
+ other.hour,
+ other.minute,
+ other.second,
+ other.microsecond,
+ ))
+ }
+}
+
+impl PartialOrd for DateTimeInfo<'_> {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ (
+ self.year,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ )
+ .partial_cmp(&(
+ other.year,
+ other.month,
+ other.day,
+ other.hour,
+ other.minute,
+ other.second,
+ other.microsecond,
+ ))
+ }
+}
+
+pub fn get_tz_name<'py>(py: Python, dt: &'py PyAny) -> PyResult<&'py str> {
+ let tz: &str = "";
+
+ if !PyDateTime::is_type_of(dt) {
+ return Ok(tz);
+ }
+
+ let tzinfo = dt.getattr("tzinfo");
+
+ match tzinfo {
+ Err(_) => Ok(tz),
+ Ok(tzinfo) => {
+ if tzinfo.is_none() {
+ return Ok(tz);
+ }
+ if tzinfo.hasattr(intern!(py, "key")).unwrap_or(false) {
+ // zoneinfo timezone
+ let tzname: &PyString = tzinfo
+ .getattr(intern!(py, "key"))
+ .unwrap()
+ .downcast()
+ .unwrap();
+
+ return tzname.to_str();
+ } else if tzinfo.hasattr(intern!(py, "name")).unwrap_or(false) {
+ // Pendulum timezone
+ let tzname: &PyString = tzinfo
+ .getattr(intern!(py, "name"))
+ .unwrap()
+ .downcast()
+ .unwrap();
+
+ return tzname.to_str();
+ } else if tzinfo.hasattr(intern!(py, "zone")).unwrap_or(false) {
+ // pytz timezone
+ let tzname: &PyString = tzinfo
+ .getattr(intern!(py, "zone"))
+ .unwrap()
+ .downcast()
+ .unwrap();
+
+ return tzname.to_str();
+ }
+
+ Ok(tz)
+ }
+ }
+}
+
+pub fn get_offset(dt: &PyAny) -> PyResult<i32> {
+ if !PyDateTime::is_type_of(dt) {
+ return Ok(0);
+ }
+
+ let tzinfo = dt.getattr("tzinfo")?;
+
+ if tzinfo.is_none() {
+ return Ok(0);
+ }
+
+ let offset: &PyDelta = tzinfo.call_method1("utcoffset", (dt,))?.downcast()?;
+
+ Ok(offset.get_days() * SECS_PER_DAY as i32 + offset.get_seconds())
+}
+
+#[pyfunction]
+pub fn is_leap(year: i32) -> PyResult<bool> {
+ Ok(helpers::is_leap(year))
+}
+
+#[pyfunction]
+pub fn is_long_year(year: i32) -> PyResult<bool> {
+ Ok(helpers::is_long_year(year))
+}
+
+#[pyfunction]
+pub fn week_day(year: i32, month: u32, day: u32) -> PyResult<u32> {
+ Ok(helpers::week_day(year, month, day))
+}
+
+#[pyfunction]
+pub fn days_in_year(year: i32) -> PyResult<u32> {
+ Ok(helpers::days_in_year(year))
+}
+
+#[pyfunction]
+pub fn local_time(
+ unix_time: f64,
+ utc_offset: isize,
+ microsecond: usize,
+) -> PyResult<(usize, usize, usize, usize, usize, usize, usize)> {
+ Ok(helpers::local_time(unix_time, utc_offset, microsecond))
+}
+
+#[pyfunction]
+pub fn precise_diff<'py>(py: Python, dt1: &'py PyAny, dt2: &'py PyAny) -> PyResult<PreciseDiff> {
+ let mut sign = 1;
+ let mut dtinfo1 = DateTimeInfo {
+ year: dt1.downcast::<PyDate>()?.get_year(),
+ month: i32::from(dt1.downcast::<PyDate>()?.get_month()),
+ day: i32::from(dt1.downcast::<PyDate>()?.get_day()),
+ hour: 0,
+ minute: 0,
+ second: 0,
+ microsecond: 0,
+ total_seconds: 0,
+ tz: get_tz_name(py, dt1)?,
+ offset: get_offset(dt1)?,
+ is_datetime: PyDateTime::is_type_of(dt1),
+ };
+ let mut dtinfo2 = DateTimeInfo {
+ year: dt2.downcast::<PyDate>()?.get_year(),
+ month: i32::from(dt2.downcast::<PyDate>()?.get_month()),
+ day: i32::from(dt2.downcast::<PyDate>()?.get_day()),
+ hour: 0,
+ minute: 0,
+ second: 0,
+ microsecond: 0,
+ total_seconds: 0,
+ tz: get_tz_name(py, dt2)?,
+ offset: get_offset(dt2)?,
+ is_datetime: PyDateTime::is_type_of(dt2),
+ };
+ let in_same_tz: bool = dtinfo1.tz == dtinfo2.tz && !dtinfo1.tz.is_empty();
+ let mut total_days = helpers::day_number(dtinfo2.year, dtinfo2.month as u8, dtinfo2.day as u8)
+ - helpers::day_number(dtinfo1.year, dtinfo1.month as u8, dtinfo1.day as u8);
+
+ if dtinfo1.is_datetime {
+ let dt1dt: &PyDateTime = dt1.downcast()?;
+
+ dtinfo1.hour = i32::from(dt1dt.get_hour());
+ dtinfo1.minute = i32::from(dt1dt.get_minute());
+ dtinfo1.second = i32::from(dt1dt.get_second());
+ dtinfo1.microsecond = dt1dt.get_microsecond() as i32;
+
+ if !in_same_tz && dtinfo1.offset != 0 || total_days == 0 {
+ dtinfo1.hour -= dtinfo1.offset / SECS_PER_HOUR as i32;
+ dtinfo1.offset %= SECS_PER_HOUR as i32;
+ dtinfo1.minute -= dtinfo1.offset / SECS_PER_MIN as i32;
+ dtinfo1.offset %= SECS_PER_MIN as i32;
+ dtinfo1.second -= dtinfo1.offset;
+
+ if dtinfo1.second < 0 {
+ dtinfo1.second += 60;
+ dtinfo1.minute -= 1;
+ } else if dtinfo1.second > 60 {
+ dtinfo1.second -= 60;
+ dtinfo1.minute += 1;
+ }
+
+ if dtinfo1.minute < 0 {
+ dtinfo1.minute += 60;
+ dtinfo1.hour -= 1;
+ } else if dtinfo1.minute > 60 {
+ dtinfo1.minute -= 60;
+ dtinfo1.hour += 1;
+ }
+
+ if dtinfo1.hour < 0 {
+ dtinfo1.hour += 24;
+ dtinfo1.day -= 1;
+ } else if dtinfo1.hour > 24 {
+ dtinfo1.hour -= 24;
+ dtinfo1.day += 1;
+ }
+ }
+
+ dtinfo1.total_seconds = dtinfo1.hour * SECS_PER_HOUR as i32
+ + dtinfo1.minute * SECS_PER_MIN as i32
+ + dtinfo1.second;
+ }
+
+ if dtinfo2.is_datetime {
+ let dt2dt: &PyDateTime = dt2.downcast()?;
+
+ dtinfo2.hour = i32::from(dt2dt.get_hour());
+ dtinfo2.minute = i32::from(dt2dt.get_minute());
+ dtinfo2.second = i32::from(dt2dt.get_second());
+ dtinfo2.microsecond = dt2dt.get_microsecond() as i32;
+
+ if !in_same_tz && dtinfo2.offset != 0 || total_days == 0 {
+ dtinfo2.hour -= dtinfo2.offset / SECS_PER_HOUR as i32;
+ dtinfo2.offset %= SECS_PER_HOUR as i32;
+ dtinfo2.minute -= dtinfo2.offset / SECS_PER_MIN as i32;
+ dtinfo2.offset %= SECS_PER_MIN as i32;
+ dtinfo2.second -= dtinfo2.offset;
+
+ if dtinfo2.second < 0 {
+ dtinfo2.second += 60;
+ dtinfo2.minute -= 1;
+ } else if dtinfo2.second > 60 {
+ dtinfo2.second -= 60;
+ dtinfo2.minute += 1;
+ }
+
+ if dtinfo2.minute < 0 {
+ dtinfo2.minute += 60;
+ dtinfo2.hour -= 1;
+ } else if dtinfo2.minute > 60 {
+ dtinfo2.minute -= 60;
+ dtinfo2.hour += 1;
+ }
+
+ if dtinfo2.hour < 0 {
+ dtinfo2.hour += 24;
+ dtinfo2.day -= 1;
+ } else if dtinfo2.hour > 24 {
+ dtinfo2.hour -= 24;
+ dtinfo2.day += 1;
+ }
+ }
+
+ dtinfo2.total_seconds = dtinfo2.hour * SECS_PER_HOUR as i32
+ + dtinfo2.minute * SECS_PER_MIN as i32
+ + dtinfo2.second;
+ }
+
+ if dtinfo1 > dtinfo2 {
+ sign = -1;
+ (dtinfo1, dtinfo2) = (dtinfo2, dtinfo1);
+
+ total_days = -total_days;
+ }
+
+ let mut year_diff = dtinfo2.year - dtinfo1.year;
+ let mut month_diff = dtinfo2.month - dtinfo1.month;
+ let mut day_diff = dtinfo2.day - dtinfo1.day;
+ let mut hour_diff = dtinfo2.hour - dtinfo1.hour;
+ let mut minute_diff = dtinfo2.minute - dtinfo1.minute;
+ let mut second_diff = dtinfo2.second - dtinfo1.second;
+ let mut microsecond_diff = dtinfo2.microsecond - dtinfo1.microsecond;
+
+ if microsecond_diff < 0 {
+ microsecond_diff += 1_000_000;
+ second_diff -= 1;
+ }
+
+ if second_diff < 0 {
+ second_diff += 60;
+ minute_diff -= 1;
+ }
+
+ if minute_diff < 0 {
+ minute_diff += 60;
+ hour_diff -= 1;
+ }
+
+ if hour_diff < 0 {
+ hour_diff += 24;
+ day_diff -= 1;
+ }
+
+ if day_diff < 0 {
+ // If we have a difference in days,
+ // we have to check if they represent months
+ let mut year = dtinfo2.year;
+ let mut month = dtinfo2.month;
+
+ if month == 1 {
+ month = 12;
+ year -= 1;
+ } else {
+ month -= 1;
+ }
+
+ let leap = helpers::is_leap(year);
+
+ let days_in_last_month = DAYS_PER_MONTHS[usize::from(leap)][month as usize];
+ let days_in_month =
+ DAYS_PER_MONTHS[usize::from(helpers::is_leap(dtinfo2.year))][dtinfo2.month as usize];
+
+ match day_diff.cmp(&(days_in_month - days_in_last_month)) {
+ Ordering::Less => {
+ // We don't have a full month, we calculate days
+ if days_in_last_month < dtinfo1.day {
+ day_diff += dtinfo1.day;
+ } else {
+ day_diff += days_in_last_month;
+ }
+ }
+ Ordering::Equal => {
+ // We have exactly a full month
+ // We remove the days difference
+ // and add one to the months difference
+ day_diff = 0;
+ month_diff += 1;
+ }
+ Ordering::Greater => {
+ // We have a full month
+ day_diff += days_in_last_month;
+ }
+ }
+
+ month_diff -= 1;
+ }
+
+ if month_diff < 0 {
+ month_diff += 12;
+ year_diff -= 1;
+ }
+
+ Ok(PreciseDiff {
+ years: year_diff * sign,
+ months: month_diff * sign,
+ days: day_diff * sign,
+ hours: hour_diff * sign,
+ minutes: minute_diff * sign,
+ seconds: second_diff * sign,
+ microseconds: microsecond_diff * sign,
+ total_days: total_days * sign,
+ })
+}
diff --git a/rust/src/python/mod.rs b/rust/src/python/mod.rs
new file mode 100644
index 0000000..8d3cd41
--- /dev/null
+++ b/rust/src/python/mod.rs
@@ -0,0 +1,27 @@
+use pyo3::prelude::*;
+
+mod helpers;
+mod parsing;
+mod types;
+
+use helpers::{days_in_year, is_leap, is_long_year, local_time, precise_diff, week_day};
+use parsing::parse_iso8601;
+use types::{Duration, PreciseDiff};
+
+#[pymodule]
+pub fn _pendulum(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
+ m.add_function(wrap_pyfunction!(days_in_year, m)?)?;
+ m.add_function(wrap_pyfunction!(is_leap, m)?)?;
+ m.add_function(wrap_pyfunction!(is_long_year, m)?)?;
+ m.add_function(wrap_pyfunction!(local_time, m)?)?;
+ m.add_function(wrap_pyfunction!(week_day, m)?)?;
+ m.add_function(wrap_pyfunction!(parse_iso8601, m)?)?;
+ m.add_function(wrap_pyfunction!(precise_diff, m)?)?;
+ m.add_class::<Duration>()?;
+ m.add_class::<PreciseDiff>()?;
+
+ #[cfg(not(feature = "mimalloc"))]
+ m.setattr("__pendulum_default_allocator__", true)?; // uses setattr so this is not in __all__
+
+ Ok(())
+}
diff --git a/rust/src/python/parsing.rs b/rust/src/python/parsing.rs
new file mode 100644
index 0000000..48fa64c
--- /dev/null
+++ b/rust/src/python/parsing.rs
@@ -0,0 +1,117 @@
+use pyo3::exceptions;
+use pyo3::prelude::*;
+use pyo3::types::PyDate;
+use pyo3::types::PyDateTime;
+use pyo3::types::PyTime;
+
+use crate::parsing::Parser;
+use crate::python::types::{Duration, FixedTimezone};
+
+#[pyfunction]
+pub fn parse_iso8601(py: Python, input: &str) -> PyResult<PyObject> {
+ let parsed = Parser::new(input).parse();
+
+ match parsed {
+ Ok(parsed) => match (parsed.datetime, parsed.duration, parsed.second_datetime) {
+ (Some(datetime), None, None) => match (datetime.has_date, datetime.has_time) {
+ (true, true) => match datetime.offset {
+ Some(offset) => {
+ let dt = PyDateTime::new(
+ py,
+ datetime.year as i32,
+ datetime.month as u8,
+ datetime.day as u8,
+ datetime.hour as u8,
+ datetime.minute as u8,
+ datetime.second as u8,
+ datetime.microsecond,
+ Some(
+ Py::new(py, FixedTimezone::new(offset, datetime.tzname))?
+ .to_object(py)
+ .extract(py)?,
+ ),
+ )?;
+
+ Ok(dt.to_object(py))
+ }
+ None => {
+ let dt = PyDateTime::new(
+ py,
+ datetime.year as i32,
+ datetime.month as u8,
+ datetime.day as u8,
+ datetime.hour as u8,
+ datetime.minute as u8,
+ datetime.second as u8,
+ datetime.microsecond,
+ None,
+ )?;
+
+ Ok(dt.to_object(py))
+ }
+ },
+ (true, false) => {
+ let dt = PyDate::new(
+ py,
+ datetime.year as i32,
+ datetime.month as u8,
+ datetime.day as u8,
+ )?;
+
+ Ok(dt.to_object(py))
+ }
+ (false, true) => match datetime.offset {
+ Some(offset) => {
+ let dt = PyTime::new(
+ py,
+ datetime.hour as u8,
+ datetime.minute as u8,
+ datetime.second as u8,
+ datetime.microsecond,
+ Some(
+ Py::new(py, FixedTimezone::new(offset, datetime.tzname))?
+ .to_object(py)
+ .extract(py)?,
+ ),
+ )?;
+
+ Ok(dt.to_object(py))
+ }
+ None => {
+ let dt = PyTime::new(
+ py,
+ datetime.hour as u8,
+ datetime.minute as u8,
+ datetime.second as u8,
+ datetime.microsecond,
+ None,
+ )?;
+
+ Ok(dt.to_object(py))
+ }
+ },
+ (_, _) => Err(exceptions::PyValueError::new_err(
+ "Parsing error".to_string(),
+ )),
+ },
+ (None, Some(duration), None) => Ok(Py::new(
+ py,
+ Duration::new(
+ Some(duration.years),
+ Some(duration.months),
+ Some(duration.weeks),
+ Some(duration.days),
+ Some(duration.hours),
+ Some(duration.minutes),
+ Some(duration.seconds),
+ Some(duration.microseconds),
+ ),
+ )?
+ .to_object(py)),
+ (_, _, _) => Err(exceptions::PyValueError::new_err(
+ "Not yet implemented".to_string(),
+ )),
+ },
+ Err(error) => Err(exceptions::PyValueError::new_err(error.to_string())),
+ }
+}
diff --git a/rust/src/python/types/duration.rs b/rust/src/python/types/duration.rs
new file mode 100644
index 0000000..fc18f4e
--- /dev/null
+++ b/rust/src/python/types/duration.rs
@@ -0,0 +1,59 @@
+use pyo3::prelude::*;
+
+#[pyclass(module = "_pendulum")]
+pub struct Duration {
+ #[pyo3(get, set)]
+ pub years: u32,
+ #[pyo3(get, set)]
+ pub months: u32,
+ #[pyo3(get, set)]
+ pub weeks: u32,
+ #[pyo3(get, set)]
+ pub days: u32,
+ #[pyo3(get, set)]
+ pub hours: u32,
+ #[pyo3(get, set)]
+ pub minutes: u32,
+ #[pyo3(get, set)]
+ pub seconds: u32,
+ #[pyo3(get, set)]
+ pub microseconds: u32,
+}
+
+#[pymethods]
+impl Duration {
+ #[new]
+ #[pyo3(signature = (years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, microseconds=0))]
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ years: Option<u32>,
+ months: Option<u32>,
+ weeks: Option<u32>,
+ days: Option<u32>,
+ hours: Option<u32>,
+ minutes: Option<u32>,
+ seconds: Option<u32>,
+ microseconds: Option<u32>,
+ ) -> Self {
+ Self {
+ years: years.unwrap_or(0),
+ months: months.unwrap_or(0),
+ weeks: weeks.unwrap_or(0),
+ days: days.unwrap_or(0),
+ hours: hours.unwrap_or(0),
+ minutes: minutes.unwrap_or(0),
+ seconds: seconds.unwrap_or(0),
+ microseconds: microseconds.unwrap_or(0),
+ }
+ }
+
+ #[getter]
+ fn remaining_days(&self) -> PyResult<u32> {
+ Ok(self.days)
+ }
+
+ #[getter]
+ fn remaining_seconds(&self) -> PyResult<u32> {
+ Ok(self.seconds)
+ }
+}
diff --git a/rust/src/python/types/interval.rs b/rust/src/python/types/interval.rs
new file mode 100644
index 0000000..7137493
--- /dev/null
+++ b/rust/src/python/types/interval.rs
@@ -0,0 +1,46 @@
+use pyo3::prelude::*;
+
+use pyo3::types::PyDelta;
+
+#[pyclass(extends=PyDelta)]
+#[derive(Default)]
+pub struct Interval {
+ pub days: i32,
+ pub seconds: i32,
+ pub microseconds: i32,
+}
+
+#[pymethods]
+impl Interval {
+ #[new]
+ #[pyo3(signature = (days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0))]
+ pub fn new(
+ py: Python,
+ days: Option<i32>,
+ seconds: Option<i32>,
+ microseconds: Option<i32>,
+ milliseconds: Option<i32>,
+ minutes: Option<i32>,
+ hours: Option<i32>,
+ weeks: Option<i32>,
+ ) -> PyResult<Self> {
+ println!("{} days", 31);
+ PyDelta::new(
+ py,
+ days.unwrap_or(0),
+ seconds.unwrap_or(0),
+ microseconds.unwrap_or(0),
+ true,
+ )?;
+
+ let f = Ok(Self {
+ days: days.unwrap_or(0),
+ seconds: seconds.unwrap_or(0),
+ microseconds: microseconds.unwrap_or(0),
+ });
+
+ println!("{} days", 31);
+
+ f
+ }
+}
diff --git a/rust/src/python/types/mod.rs b/rust/src/python/types/mod.rs
new file mode 100644
index 0000000..cba11df
--- /dev/null
+++ b/rust/src/python/types/mod.rs
@@ -0,0 +1,7 @@
+mod duration;
+mod precise_diff;
+mod timezone;
+
+pub use duration::Duration;
+pub use precise_diff::PreciseDiff;
+pub use timezone::FixedTimezone;
diff --git a/rust/src/python/types/precise_diff.rs b/rust/src/python/types/precise_diff.rs
new file mode 100644
index 0000000..64ca3a6
--- /dev/null
+++ b/rust/src/python/types/precise_diff.rs
@@ -0,0 +1,53 @@
+use pyo3::prelude::*;
+
+#[pyclass(module = "_pendulum")]
+pub struct PreciseDiff {
+ #[pyo3(get, set)]
+ pub years: i32,
+ #[pyo3(get, set)]
+ pub months: i32,
+ #[pyo3(get, set)]
+ pub days: i32,
+ #[pyo3(get, set)]
+ pub hours: i32,
+ #[pyo3(get, set)]
+ pub minutes: i32,
+ #[pyo3(get, set)]
+ pub seconds: i32,
+ #[pyo3(get, set)]
+ pub microseconds: i32,
+ #[pyo3(get, set)]
+ pub total_days: i32,
+}
+
+#[pymethods]
+impl PreciseDiff {
+ #[new]
+ #[pyo3(signature = (years=0, months=0, days=0, hours=0, minutes=0, seconds=0, microseconds=0, total_days=0))]
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ years: Option<i32>,
+ months: Option<i32>,
+ days: Option<i32>,
+ hours: Option<i32>,
+ minutes: Option<i32>,
+ seconds: Option<i32>,
+ microseconds: Option<i32>,
+ total_days: Option<i32>,
+ ) -> Self {
+ Self {
+ years: years.unwrap_or(0),
+ months: months.unwrap_or(0),
+ days: days.unwrap_or(0),
+ hours: hours.unwrap_or(0),
+ minutes: minutes.unwrap_or(0),
+ seconds: seconds.unwrap_or(0),
+ microseconds: microseconds.unwrap_or(0),
+ total_days: total_days.unwrap_or(0),
+ }
+ }
+
+ fn __repr__(&self) -> String {
+ format!("PreciseDiff(years={}, months={}, days={}, hours={}, minutes={}, seconds={}, microseconds={}, total_days={})", self.years, self.months, self.days, self.hours, self.minutes, self.seconds, self.microseconds, self.total_days)
+ }
+}
diff --git a/rust/src/python/types/timezone.rs b/rust/src/python/types/timezone.rs
new file mode 100644
index 0000000..1a8bbad
--- /dev/null
+++ b/rust/src/python/types/timezone.rs
@@ -0,0 +1,52 @@
+use pyo3::prelude::*;
+use pyo3::types::{PyDelta, PyDict, PyTzInfo};
+
+#[pyclass(module = "_pendulum", extends = PyTzInfo)]
+#[derive(Clone)]
+pub struct FixedTimezone {
+ offset: i32,
+ name: Option<String>,
+}
+
+#[pymethods]
+impl FixedTimezone {
+ #[new]
+ pub fn new(offset: i32, name: Option<String>) -> Self {
+ Self { offset, name }
+ }
+
+ fn utcoffset<'p>(&self, py: Python<'p>, _dt: &PyAny) -> PyResult<&'p PyDelta> {
+ PyDelta::new(py, 0, self.offset, 0, true)
+ }
+
+ fn tzname(&self, _dt: &PyAny) -> String {
+ self.__str__()
+ }
+
+ fn dst<'p>(&self, py: Python<'p>, _dt: &PyAny) -> PyResult<&'p PyDelta> {
+ PyDelta::new(py, 0, 0, 0, true)
+ }
+
+ fn __repr__(&self) -> String {
+ format!(
+ "FixedTimezone({}, name=\"{}\")",
+ self.offset,
+ self.__str__()
+ )
+ }
+
+ fn __str__(&self) -> String {
+ if let Some(n) = &self.name {
+ n.clone()
+ } else {
+ let sign = if self.offset < 0 { "-" } else { "+" };
+ let minutes = self.offset.abs() / 60;
+ let (hour, minute) = (minutes / 60, minutes % 60);
+ format!("{sign}{hour:.2}:{minute:.2}")
+ }
+ }
+
+ fn __deepcopy__(&self, py: Python, _memo: &PyDict) -> PyResult<Py<Self>> {
+ Py::new(py, self.clone())
+ }
+}