From 665666d6f4213da8db57ebb480947b7caf1fe382 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 17 Dec 2023 15:36:26 +0100 Subject: Merging upstream version 3.0.0. Signed-off-by: Daniel Baumann --- rust/.cargo/config.toml | 15 + rust/Cargo.lock | 318 ++++++++++++ rust/Cargo.toml | 22 + rust/src/constants.rs | 56 +++ rust/src/helpers.rs | 122 +++++ rust/src/lib.rs | 12 + rust/src/parsing.rs | 905 ++++++++++++++++++++++++++++++++++ rust/src/python/helpers.rs | 388 +++++++++++++++ rust/src/python/mod.rs | 27 + rust/src/python/parsing.rs | 117 +++++ rust/src/python/types/duration.rs | 59 +++ rust/src/python/types/interval.rs | 46 ++ rust/src/python/types/mod.rs | 7 + rust/src/python/types/precise_diff.rs | 53 ++ rust/src/python/types/timezone.rs | 52 ++ 15 files changed, 2199 insertions(+) create mode 100644 rust/.cargo/config.toml create mode 100644 rust/Cargo.lock create mode 100644 rust/Cargo.toml create mode 100644 rust/src/constants.rs create mode 100644 rust/src/helpers.rs create mode 100644 rust/src/lib.rs create mode 100644 rust/src/parsing.rs create mode 100644 rust/src/python/helpers.rs create mode 100644 rust/src/python/mod.rs create mode 100644 rust/src/python/parsing.rs create mode 100644 rust/src/python/types/duration.rs create mode 100644 rust/src/python/types/interval.rs create mode 100644 rust/src/python/types/mod.rs create mode 100644 rust/src/python/types/precise_diff.rs create mode 100644 rust/src/python/types/timezone.rs (limited to 'rust') 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, + pub has_offset: bool, + pub tzname: Option, + 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, + pub duration: Option, + pub second_datetime: Option, +} + +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 { + 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 { + 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 { + 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), 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 { + 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 { + ( + 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 { + 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 { + Ok(helpers::is_leap(year)) +} + +#[pyfunction] +pub fn is_long_year(year: i32) -> PyResult { + Ok(helpers::is_long_year(year)) +} + +#[pyfunction] +pub fn week_day(year: i32, month: u32, day: u32) -> PyResult { + Ok(helpers::week_day(year, month, day)) +} + +#[pyfunction] +pub fn days_in_year(year: i32) -> PyResult { + 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 { + let mut sign = 1; + let mut dtinfo1 = DateTimeInfo { + year: dt1.downcast::()?.get_year(), + month: i32::from(dt1.downcast::()?.get_month()), + day: i32::from(dt1.downcast::()?.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::()?.get_year(), + month: i32::from(dt2.downcast::()?.get_month()), + day: i32::from(dt2.downcast::()?.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::()?; + m.add_class::()?; + + #[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 { + 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, + months: Option, + weeks: Option, + days: Option, + hours: Option, + minutes: Option, + seconds: Option, + microseconds: Option, + ) -> 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 { + Ok(self.days) + } + + #[getter] + fn remaining_seconds(&self) -> PyResult { + 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, + seconds: Option, + microseconds: Option, + milliseconds: Option, + minutes: Option, + hours: Option, + weeks: Option, + ) -> PyResult { + 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, + months: Option, + days: Option, + hours: Option, + minutes: Option, + seconds: Option, + microseconds: Option, + total_days: Option, + ) -> 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, +} + +#[pymethods] +impl FixedTimezone { + #[new] + pub fn new(offset: i32, name: Option) -> 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::new(py, self.clone()) + } +} -- cgit v1.2.3