From 71f7ddec831098458a0a349bf936b06ca22b380a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Mon, 27 Feb 2023 11:05:25 +0100 Subject: [PATCH 01/19] Improve performances by using Rust --- .gitignore | 1 + Cargo.lock | 315 +++++++++++++++++++++++++++ Cargo.toml | 18 ++ build.py | 22 +- pyproject.toml | 5 +- rust/constants.rs | 77 +++++++ rust/helpers.rs | 115 ++++++++++ rust/lib.rs | 78 +++++++ rust/parsing.rs | 494 +++++++++++++++++++++++++++++++++++++++++++ rust/types/mod.rs | 3 + rust/types/tzinfo.rs | 40 ++++ 11 files changed, 1166 insertions(+), 2 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 rust/constants.rs create mode 100644 rust/helpers.rs create mode 100644 rust/lib.rs create mode 100644 rust/parsing.rs create mode 100644 rust/types/mod.rs create mode 100644 rust/types/tzinfo.rs diff --git a/.gitignore b/.gitignore index bb25f8b3..d7309a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ setup.py # editor .vscode +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..a067ca16 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,315 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[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 = "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 = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[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 = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[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 = "pendulum" +version = "3.0.0-alpha-1" +dependencies = [ + "iso8601", + "nom", + "pyo3", +] + +[[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.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06a3d8e8a46ab2738109347433cb7b96dffda2e4a218b03ef27090238886b147" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75439f995d07ddfad42b192dfcf3bc66a7ecfd8b4a1f5f6f046aa5c2c5d7677d" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839526a5c07a17ff44823679b68add4a58004de00512a95b6c1c98a6dcac0ee5" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd44cf207476c6a9760c4653559be4f206efafb924d3e4cbf2721475fc0d6cc5" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1f43d8e30460f36350d18631ccf85ded64c059829208fe680904c65bcd0a4c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..7d3b0fd0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pendulum" +version = "3.0.0-alpha-1" +edition = "2021" + +[lib] +name = "_pendulum" +crate-type = ["cdylib"] +path = "rust/lib.rs" + +[dependencies] +iso8601 = "0.6.1" +nom = "7.1.3" +pyo3 = { version = "0.18.1", features = ["extension-module"] } + +[features] +extension-module = ["pyo3/extension-module"] +default = ["extension-module"] diff --git a/build.py b/build.py index 8a72beb9..8fb56882 100644 --- a/build.py +++ b/build.py @@ -1,4 +1,6 @@ +import shutil import subprocess +import zipfile from pathlib import Path @@ -7,6 +9,10 @@ def meson(*args): subprocess.call(["meson", *list(args)]) +def maturin(*args): + subprocess.call(["maturin"] + list(args)) + + def _build(): build_dir = Path(__file__).parent.joinpath("build") build_dir.mkdir(parents=True, exist_ok=True) @@ -15,6 +21,19 @@ def _build(): meson("compile", "-C", build_dir.as_posix()) meson("install", "-C", build_dir.as_posix()) + wheels_dir = Path(__file__).parent.joinpath("target/wheels") + if wheels_dir.exists(): + shutil.rmtree(wheels_dir) + + maturin("build", "-r") + + wheel = list(wheels_dir.glob("*.whl"))[0] + with zipfile.ZipFile(wheel.as_posix()) as whl: + whl.extractall(wheels_dir.as_posix()) + + for extension in wheels_dir.rglob("**/*.so"): + shutil.copyfile(extension, Path(__file__).parent.joinpath(extension.name)) + def build(setup_kwargs): """ @@ -22,11 +41,12 @@ def build(setup_kwargs): """ try: _build() - except Exception: + except Exception as e: print( " Unable to build C extensions, " "Pendulum will use the pure python version of the extensions." ) + print(e) if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index 91bd7005..35e63f99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,9 @@ include = [ { path = "pendulum/_extensions/*.pyd", format = "wheel" }, { path = "pendulum/parsing/*.so", format = "wheel" }, { path = "pendulum/parsing/*.pyd", format = "wheel" }, + # Rust extension + { path = "_pendulum*.so", format = "wheel" }, + { path = "_pendulum*.pyd", format = "wheel" }, ] @@ -219,5 +222,5 @@ omit = [ ] [build-system] -requires = ["poetry-core>=1.1.0a6", "meson", "ninja"] +requires = ["poetry-core>=1.1.0a6", "meson", "ninja", "maturin>=0.14,<0.15"] build-backend = "poetry.core.masonry.api" diff --git a/rust/constants.rs b/rust/constants.rs new file mode 100644 index 00000000..e48420b5 --- /dev/null +++ b/rust/constants.rs @@ -0,0 +1,77 @@ +pub const EPOCH_YEAR: u32 = 1970; + +pub const DAYS_PER_N_YEAR: u32 = 365; +pub const DAYS_PER_L_YEAR: u32 = 366; + +pub const USECS_PER_SEC: u32 = 1000000; + +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 = 146097; +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. +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 + 1 * 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, +]; + +pub const MONTHS_PER_YEAR: u32 = 12; + +// 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_SUNDAY: usize = 0; +pub const TM_MONDAY: usize = 1; +pub const TM_TUESDAY: usize = 2; +pub const TM_WEDNESDAY: usize = 3; +pub const TM_THURSDAY: usize = 4; +pub const TM_FRIDAY: usize = 5; +pub const TM_SATURDAY: usize = 6; + +pub const TM_JANUARY: usize = 0; +pub const TM_FEBRUARY: usize = 1; +pub const TM_MARCH: usize = 2; +pub const TM_APRIL: usize = 3; +pub const TM_MAY: usize = 4; +pub const TM_JUNE: usize = 5; +pub const TM_JULY: usize = 6; +pub const TM_AUGUST: usize = 7; +pub const TM_SEPTEMBER: usize = 8; +pub const TM_OCTOBER: usize = 9; +pub const TM_NOVEMBER: usize = 10; +pub const TM_DECEMBER: usize = 11; diff --git a/rust/helpers.rs b/rust/helpers.rs new file mode 100644 index 00000000..55a5c09d --- /dev/null +++ b/rust/helpers.rs @@ -0,0 +1,115 @@ +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: u32) -> u32 { + return year + year / 4 - year / 100 + year / 400; +} + +pub fn is_leap(year: u32) -> bool { + year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) +} + +pub fn is_long_year(year: u32) -> bool { + (p(year) % 7 == 4) || (p(year - 1) % 7 == 3) +} + +pub fn days_in_year(year: u32) -> u32 { + if is_leap(year) { + return DAYS_PER_L_YEAR; + } + + DAYS_PER_N_YEAR +} + +pub fn week_day(year: u32, month: u32, day: u32) -> u32 { + let y: u32 = year - (month < 3) as u32; + + let w: u32 = (p(y) + DAY_OF_WEEK_TABLE[(month - 1) as usize] + day) % 7; + + if w == 0 { + return 7; + } + + w +} + +pub fn local_time( + unix_time: isize, + 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; + + // 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 += ((146097 - 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) as usize; + 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) as usize { + 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/lib.rs b/rust/lib.rs new file mode 100644 index 00000000..e3f3efbd --- /dev/null +++ b/rust/lib.rs @@ -0,0 +1,78 @@ +extern crate core; + +use pyo3::{prelude::*, types::PyDateTime}; + +mod constants; +mod helpers; +mod parsing; +mod types; + +use types::TzInfo; + +#[pyfunction] +fn is_leap(year: u32) -> PyResult { + Ok(helpers::is_leap(year)) +} + +#[pyfunction] +fn is_long_year(year: u32) -> PyResult { + Ok(helpers::is_long_year(year)) +} + +#[pyfunction] +fn week_day(year: u32, month: u32, day: u32) -> PyResult { + Ok(helpers::week_day(year, month, day)) +} + +#[pyfunction] +fn local_time( + unix_time: isize, + utc_offset: isize, + microsecond: usize, +) -> PyResult<(usize, usize, usize, usize, usize, usize, usize)> { + Ok(helpers::local_time(unix_time, utc_offset, microsecond)) +} + +#[pyfunction] +fn parse<'p>(py: Python<'p>, input: &str) -> PyResult<&'p PyDateTime> { + let parsed = parsing::Parser::new(input).parse(); + + match parsed.offset { + Some(offset) => PyDateTime::new( + py, + parsed.year as i32, + parsed.month as u8, + parsed.day as u8, + parsed.hour as u8, + parsed.minute as u8, + parsed.second as u8, + parsed.microsecond as u32, + Some( + Py::new(py, TzInfo::new(offset))? + .to_object(py) + .extract(py)?, + ), + ), + None => PyDateTime::new( + py, + parsed.year as i32, + parsed.month as u8, + parsed.day as u8, + parsed.hour as u8, + parsed.minute as u8, + parsed.second as u8, + parsed.microsecond as u32, + None, + ), + } +} + +#[pymodule] +fn _pendulum(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + 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, m)?)?; + Ok(()) +} diff --git a/rust/parsing.rs b/rust/parsing.rs new file mode 100644 index 00000000..3983a347 --- /dev/null +++ b/rust/parsing.rs @@ -0,0 +1,494 @@ +use core::str; +use std::str::CharIndices; + +use crate::{ + constants::{DAYS_PER_MONTHS, MONTHS_OFFSETS}, + helpers::{days_in_year, is_leap, is_long_year, week_day}, +}; + +pub struct Duration { + years: i32, +} + +pub struct Parsed { + pub is_date: bool, + pub is_time: bool, + pub is_datetime: bool, + pub is_duration: bool, + pub is_period: bool, + + ambiguous: bool, + 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, +} + +impl<'a> Parsed { + pub fn new() -> Parsed { + Parsed { + is_date: false, + is_time: false, + is_datetime: false, + is_duration: false, + is_period: false, + + ambiguous: false, + year: 0, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + microsecond: 0, + offset: None, + has_offset: false, + } + } +} + +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) -> bool { + match self.chars.next() { + Some((i, ch)) => { + self.idx = i; + self.current = ch; + true + } + None => { + self.idx = self.src.len(); + self.current = '\0'; + false + } + } + } + + /// Increments the parser by `n` characters if the end of the input + /// has not been reached. Eliminates the need for repeated 'self.inc();' + /// in code. + fn inc_n(&mut self, n: usize) -> bool { + for _ in 0..n { + if !self.inc() { + return false; + } + } + true + } + + /// Returns true if the parser has reached the end of the input. + fn end(&self) -> bool { + self.idx >= self.src.len() + } + + pub fn parse(&mut self) -> Parsed { + let mut parsed = Parsed::new(); + parsed.is_date = true; + + for _ in 0..4 { + if self.end() { + // TODO: Error + } + + if self.current >= '0' && self.current <= '9' { + parsed.year = 10 * parsed.year + self.current.to_digit(10).unwrap() as u32; + self.inc(); + } else { + // TODO: Error + return parsed; + } + } + + let mut leap = is_leap(parsed.year); + let mut separators: u32 = 0; + let mut week: u32 = 0; + let mut monthday: u32 = 0; + let mut i = 0; + + // Optional separator + if self.current == '-' { + separators += 1; + self.inc(); + } + + // Checking for week dates + if self.current == 'W' { + self.inc(); + + let mut weekday: u32 = 1; + + while !self.end() && self.current != ' ' && self.current != 'T' { + if self.current == '-' { + separators += 1; + self.inc(); + continue; + } + + week = 10 * week + self.current.to_digit(10).unwrap() as u32; + + i += 1; + self.inc(); + } + + match i { + 2 => (), // Only week number + 3 => { + // Week with week day + if !(separators == 0 || separators == 2) { + // We should have to or no separators + // TODO: Invalid week date + return parsed; + } + + weekday = week % 10; + week /= 10; + } + _ => { + // TODO: Invalid week date + return parsed; + } + } + + // Checks + if week > 53 || week > 52 && !is_long_year(parsed.year) { + // TODO: Invalid week number + return parsed; + } + + if weekday > 7 { + // TODO: Invalid week day + return parsed; + } + + // Calculating ordinal day + let mut ordinal: u32 = week * 7 + weekday - (week_day(parsed.year, 1, 4) + 3); + + if ordinal < 1 { + // Previous year + ordinal += days_in_year(parsed.year - 1); + parsed.year -= 1; + leap = is_leap(parsed.year); + } + + if ordinal > days_in_year(parsed.year) { + // Next year + ordinal -= days_in_year(parsed.year); + parsed.year += 1; + leap = is_leap(parsed.year); + } + + for j in 1..14 { + if ordinal <= MONTHS_OFFSETS[leap as usize][j] as u32 { + parsed.day = ordinal - MONTHS_OFFSETS[leap as usize][j - 1] as u32; + parsed.month = (j - 1) as u32; + + break; + } + } + } else { + // At this point we need to check the number + // of characters until the end of the date part + // (or the end of the string). + // + // If two, we have only a month if there is a separator, it may be a time otherwise. + // If three, we have an ordinal date. + // If four, we have a complete date + while !self.end() && self.current != ' ' && self.current != 'T' { + if self.current == '-' { + separators += 1; + self.inc(); + continue; + } + + if !(self.current >= '0' && self.current <= '9') { + // TODO: Error + return parsed; + } + + monthday = 10 * monthday + self.current.to_digit(10).unwrap() as u32; + self.inc(); + + i += 1; + } + + match i { + 0 => (), + 2 => { + if separators == 0 { + // TODO: ambiguous + parsed.ambiguous = true; + (); + } + + parsed.month = monthday; + } + 3 => { + // Ordinal day + if separators > 1 { + // TODO: Invalid ordinal date + return parsed; + } + + if monthday < 1 || monthday > MONTHS_OFFSETS[leap as usize][13] as u32 { + // TODO: Invalid ordinal day for year + return parsed; + } + + for j in 1..14 { + if monthday < MONTHS_OFFSETS[leap as usize][j] as u32 { + parsed.day = monthday - MONTHS_OFFSETS[leap as usize][j - 1] as u32; + parsed.month = (j - 1) as u32; + + break; + } + } + } + 4 => { + // Month and day + parsed.month = monthday as u32 / 100; + parsed.day = monthday as u32 % 100; + } + _ => { + // TODO: Error + return parsed; + } + } + } + + if separators > 0 && monthday == 0 && week == 0 { + // TODO: Invalid date + + return parsed; + } + + if parsed.month > 12 { + // TODO: Invalid month + return parsed; + } + + if parsed.day > DAYS_PER_MONTHS[leap as usize][parsed.month as usize] as u32 { + // TODO: Invalid day for month + return parsed; + } + + separators = 0; + + if self.current == 'T' || self.current == ' ' { + if parsed.ambiguous { + // TODO: Invalid date + return parsed; + } + + // We have time so we have a datetime + parsed.is_date = false; + parsed.is_datetime = true; + + if !self.inc() { + // TODO: invalid date + return parsed; + } + + // Grabbing time information + i = 0; + let mut time = 0; + while !self.end() + && self.current != '.' + && self.current != ',' + && self.current != 'Z' + && self.current != '+' + && self.current != '-' + { + if self.current == ':' { + separators += 1; + self.inc(); + continue; + } + + if !(self.current >= '0' && self.current <= '9') { + // TODO: Invalid time + return parsed; + } + + time = 10 * time + self.current.to_digit(10).unwrap(); + i += 1; + self.inc(); + } + + match i { + 2 => { + // Hours only + if separators > 0 { + // Extraneous separators + // TODO: Invalid time + return parsed; + } + + parsed.hour = time; + } + 4 => { + // Hours and minutes + if separators > 1 { + // Extraneous separators + // TODO: Invalid time + return parsed; + } + + parsed.hour = time / 100; + parsed.minute = time % 100; + } + 6 => { + // Hours, minutes and seconds + if separators > 2 { + // Extraneous separators + // TODO: Invalid time + return parsed; + } + + parsed.hour = time / 10000; + parsed.minute = time / 100 % 100; + parsed.second = time % 100; + } + _ => { + // TODO: Invalid time + return parsed; + } + } + + // Checks + if parsed.hour > 23 { + // TODO: Invalid hour + return parsed; + } + + if parsed.minute > 59 { + // TODO: Invalid minute + return parsed; + } + + if parsed.second > 59 { + // TODO: Invalid second + return parsed; + } + + // Subsecond + if self.current == '.' || self.current == ',' { + self.inc(); + + time = 0; + i = 0; + while !self.end() + && self.current != 'Z' + && self.current != '+' + && self.current != '-' + { + if !(self.current >= '0' && self.current <= '9') { + // TODO: Invalid time + return parsed; + } + + time = 10 * time + self.current.to_digit(10).unwrap(); + i += 1; + self.inc(); + } + + // Adjust to microseconds + if i > 6 { + parsed.microsecond = time / 10u32.pow(i - 6); + } else if i <= 6 { + parsed.microsecond = time * 10u32.pow(6 - i); + } + } + } + + // Timezone + if self.current == 'Z' { + parsed.has_offset = true; + self.inc(); + } else if self.current == '+' || self.current == '-' { + let tz_sign: i32 = if self.current == '+' { 1 } else { -1 }; + + parsed.has_offset = true; + self.inc(); + + i = 0; + separators = 0; + let mut offset: i32 = 0; + + while !self.end() { + if self.current == ':' { + separators += 1; + self.inc(); + continue; + } + + if !(self.current >= '0' && self.current <= '9') { + // TODO: Invalid offset + return parsed; + } + + offset = 10 * offset + self.current.to_digit(10).unwrap() as i32; + self.inc(); + i += 1; + } + + match i { + 2 => { + // hh format + if separators > 0 { + // Extraneous separators + // TODO: Invalid offset + return parsed; + } + + parsed.offset = Some(tz_sign * offset * 3600); + } + 4 => { + // hhmm format + if separators > 1 { + // Extraneous separators + // TODO: Invalid offset + return parsed; + } + + parsed.offset = Some(tz_sign * ((offset / 100 * 3600) + (offset % 100 * 60))); + } + _ => { + // Wrong format + // TODO: Invalid format + return parsed; + } + } + } + + return parsed; + } +} diff --git a/rust/types/mod.rs b/rust/types/mod.rs new file mode 100644 index 00000000..71b70c4f --- /dev/null +++ b/rust/types/mod.rs @@ -0,0 +1,3 @@ +mod tzinfo; + +pub use tzinfo::TzInfo; diff --git a/rust/types/tzinfo.rs b/rust/types/tzinfo.rs new file mode 100644 index 00000000..809736a7 --- /dev/null +++ b/rust/types/tzinfo.rs @@ -0,0 +1,40 @@ +use pyo3::prelude::*; +use pyo3::types::{PyDateTime, PyDelta, PyTzInfo}; + +#[pyclass(module = "_pendulum", extends = PyTzInfo)] +pub struct TzInfo { + seconds: i32, +} + +#[pymethods] +impl TzInfo { + #[new] + pub fn new(seconds: i32) -> Self { + Self { seconds } + } + + fn utcoffset<'p>(&self, py: Python<'p>, _dt: &PyDateTime) -> PyResult<&'p PyDelta> { + PyDelta::new(py, 0, self.seconds, 0, true) + } + + fn tzname(&self, _dt: &PyDateTime) -> String { + self.__str__() + } + + fn dst(&self, _dt: &PyDateTime) -> Option<&PyDelta> { + None + } + + fn __repr__(&self) -> String { + format!("TzInfo({})", self.__str__()) + } + + fn __str__(&self) -> String { + if self.seconds == 0 { + "UTC".to_string() + } else { + let mins = self.seconds / 60; + format!("{:+03}:{:02}", mins / 60, (mins % 60).abs()) + } + } +} From 31bc2800d9eba3896595d7b2a15d1b870f793afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Sun, 5 Mar 2023 00:11:37 +0100 Subject: [PATCH 02/19] Reorganize Rust code --- rust/lib.rs | 74 +---------------------------------- rust/python/helpers.rs | 27 +++++++++++++ rust/python/mod.rs | 19 +++++++++ rust/python/parsing.rs | 38 ++++++++++++++++++ rust/python/types/mod.rs | 3 ++ rust/python/types/timezone.rs | 48 +++++++++++++++++++++++ rust/types/mod.rs | 3 -- rust/types/tzinfo.rs | 40 ------------------- 8 files changed, 137 insertions(+), 115 deletions(-) create mode 100644 rust/python/helpers.rs create mode 100644 rust/python/mod.rs create mode 100644 rust/python/parsing.rs create mode 100644 rust/python/types/mod.rs create mode 100644 rust/python/types/timezone.rs delete mode 100644 rust/types/mod.rs delete mode 100644 rust/types/tzinfo.rs diff --git a/rust/lib.rs b/rust/lib.rs index e3f3efbd..aac55da7 100644 --- a/rust/lib.rs +++ b/rust/lib.rs @@ -1,78 +1,8 @@ extern crate core; -use pyo3::{prelude::*, types::PyDateTime}; - mod constants; mod helpers; mod parsing; -mod types; - -use types::TzInfo; - -#[pyfunction] -fn is_leap(year: u32) -> PyResult { - Ok(helpers::is_leap(year)) -} - -#[pyfunction] -fn is_long_year(year: u32) -> PyResult { - Ok(helpers::is_long_year(year)) -} - -#[pyfunction] -fn week_day(year: u32, month: u32, day: u32) -> PyResult { - Ok(helpers::week_day(year, month, day)) -} - -#[pyfunction] -fn local_time( - unix_time: isize, - utc_offset: isize, - microsecond: usize, -) -> PyResult<(usize, usize, usize, usize, usize, usize, usize)> { - Ok(helpers::local_time(unix_time, utc_offset, microsecond)) -} - -#[pyfunction] -fn parse<'p>(py: Python<'p>, input: &str) -> PyResult<&'p PyDateTime> { - let parsed = parsing::Parser::new(input).parse(); - - match parsed.offset { - Some(offset) => PyDateTime::new( - py, - parsed.year as i32, - parsed.month as u8, - parsed.day as u8, - parsed.hour as u8, - parsed.minute as u8, - parsed.second as u8, - parsed.microsecond as u32, - Some( - Py::new(py, TzInfo::new(offset))? - .to_object(py) - .extract(py)?, - ), - ), - None => PyDateTime::new( - py, - parsed.year as i32, - parsed.month as u8, - parsed.day as u8, - parsed.hour as u8, - parsed.minute as u8, - parsed.second as u8, - parsed.microsecond as u32, - None, - ), - } -} +mod python; -#[pymodule] -fn _pendulum(_py: Python<'_>, m: &PyModule) -> PyResult<()> { - 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, m)?)?; - Ok(()) -} +pub use python::_pendulum; diff --git a/rust/python/helpers.rs b/rust/python/helpers.rs new file mode 100644 index 00000000..449afefe --- /dev/null +++ b/rust/python/helpers.rs @@ -0,0 +1,27 @@ +use pyo3::prelude::*; + +use crate::helpers; + +#[pyfunction] +pub fn is_leap(year: u32) -> PyResult { + Ok(helpers::is_leap(year)) +} + +#[pyfunction] +pub fn is_long_year(year: u32) -> PyResult { + Ok(helpers::is_long_year(year)) +} + +#[pyfunction] +pub fn week_day(year: u32, month: u32, day: u32) -> PyResult { + Ok(helpers::week_day(year, month, day)) +} + +#[pyfunction] +pub fn local_time( + unix_time: isize, + utc_offset: isize, + microsecond: usize, +) -> PyResult<(usize, usize, usize, usize, usize, usize, usize)> { + Ok(helpers::local_time(unix_time, utc_offset, microsecond)) +} diff --git a/rust/python/mod.rs b/rust/python/mod.rs new file mode 100644 index 00000000..c67e3fae --- /dev/null +++ b/rust/python/mod.rs @@ -0,0 +1,19 @@ +use pyo3::prelude::*; + +mod helpers; +mod parsing; +mod types; + +use helpers::{is_leap, is_long_year, local_time, week_day}; +use parsing::parse_iso8601; + +#[pymodule] +pub fn _pendulum(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + 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)?)?; + + Ok(()) +} diff --git a/rust/python/parsing.rs b/rust/python/parsing.rs new file mode 100644 index 00000000..3e44fe34 --- /dev/null +++ b/rust/python/parsing.rs @@ -0,0 +1,38 @@ +use pyo3::{prelude::*, types::PyDateTime}; + +use crate::parsing::Parser; +use crate::python::types::FixedTimezone; + +#[pyfunction] +pub fn parse_iso8601<'p>(py: Python<'p>, input: &str) -> PyResult<&'p PyDateTime> { + let parsed = Parser::new(input).parse(); + + match parsed.offset { + Some(offset) => PyDateTime::new( + py, + parsed.year as i32, + parsed.month as u8, + parsed.day as u8, + parsed.hour as u8, + parsed.minute as u8, + parsed.second as u8, + parsed.microsecond as u32, + Some( + Py::new(py, FixedTimezone::new(offset, None))? + .to_object(py) + .extract(py)?, + ), + ), + None => PyDateTime::new( + py, + parsed.year as i32, + parsed.month as u8, + parsed.day as u8, + parsed.hour as u8, + parsed.minute as u8, + parsed.second as u8, + parsed.microsecond as u32, + None, + ), + } +} diff --git a/rust/python/types/mod.rs b/rust/python/types/mod.rs new file mode 100644 index 00000000..aac0dad4 --- /dev/null +++ b/rust/python/types/mod.rs @@ -0,0 +1,3 @@ +mod timezone; + +pub use timezone::FixedTimezone; diff --git a/rust/python/types/timezone.rs b/rust/python/types/timezone.rs new file mode 100644 index 00000000..c0688fef --- /dev/null +++ b/rust/python/types/timezone.rs @@ -0,0 +1,48 @@ +use pyo3::prelude::*; +use pyo3::types::{PyDateTime, PyDelta, PyTzInfo}; + +#[pyclass(module = "_pendulum", extends = PyTzInfo)] +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: &PyDateTime) -> PyResult<&'p PyDelta> { + PyDelta::new(py, 0, self.offset, 0, true) + } + + fn tzname(&self, _dt: &PyDateTime) -> String { + self.__str__() + } + + fn dst<'p>(&self, py: Python<'p>, _dt: &PyDateTime) -> 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 { + match self.name.clone() { + Some(n) => n, + None => { + let sign = if self.offset < 0 { "-" } else { "+" }; + let minutes = self.offset / 60; + let (hour, minute) = (minutes.abs() / 60, minutes.abs() % 60); + format!("{}{:.2}:{:.2}", sign, hour, minute) + } + } + } +} diff --git a/rust/types/mod.rs b/rust/types/mod.rs deleted file mode 100644 index 71b70c4f..00000000 --- a/rust/types/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod tzinfo; - -pub use tzinfo::TzInfo; diff --git a/rust/types/tzinfo.rs b/rust/types/tzinfo.rs deleted file mode 100644 index 809736a7..00000000 --- a/rust/types/tzinfo.rs +++ /dev/null @@ -1,40 +0,0 @@ -use pyo3::prelude::*; -use pyo3::types::{PyDateTime, PyDelta, PyTzInfo}; - -#[pyclass(module = "_pendulum", extends = PyTzInfo)] -pub struct TzInfo { - seconds: i32, -} - -#[pymethods] -impl TzInfo { - #[new] - pub fn new(seconds: i32) -> Self { - Self { seconds } - } - - fn utcoffset<'p>(&self, py: Python<'p>, _dt: &PyDateTime) -> PyResult<&'p PyDelta> { - PyDelta::new(py, 0, self.seconds, 0, true) - } - - fn tzname(&self, _dt: &PyDateTime) -> String { - self.__str__() - } - - fn dst(&self, _dt: &PyDateTime) -> Option<&PyDelta> { - None - } - - fn __repr__(&self) -> String { - format!("TzInfo({})", self.__str__()) - } - - fn __str__(&self) -> String { - if self.seconds == 0 { - "UTC".to_string() - } else { - let mins = self.seconds / 60; - format!("{:+03}:{:02}", mins / 60, (mins % 60).abs()) - } - } -} From 77387659030ded044b10da068bd91196edf5f7dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Wed, 8 Mar 2023 10:28:11 +0100 Subject: [PATCH 03/19] Improve ISO8601 parsing --- rust/parsing.rs | 781 ++++++++++++++++++++++++----------------- rust/python/parsing.rs | 71 ++-- 2 files changed, 498 insertions(+), 354 deletions(-) diff --git a/rust/parsing.rs b/rust/parsing.rs index 3983a347..f57b5257 100644 --- a/rust/parsing.rs +++ b/rust/parsing.rs @@ -1,5 +1,5 @@ use core::str; -use std::str::CharIndices; +use std::{fmt, str::CharIndices}; use crate::{ constants::{DAYS_PER_MONTHS, MONTHS_OFFSETS}, @@ -10,14 +10,20 @@ pub struct Duration { years: i32, } -pub struct Parsed { - pub is_date: bool, - pub is_time: bool, - pub is_datetime: bool, - pub is_duration: bool, - pub is_period: bool, +#[derive(Debug, Clone)] +pub struct ParseError { + index: usize, + c: char, + message: String, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} (Position: {})", self.message, self.index.to_string()) + } +} - ambiguous: bool, +pub struct ParsedDateTime { pub year: u32, pub month: u32, pub day: u32, @@ -27,18 +33,12 @@ pub struct Parsed { pub microsecond: u32, pub offset: Option, pub has_offset: bool, + pub time_is_midnight: bool, } -impl<'a> Parsed { - pub fn new() -> Parsed { - Parsed { - is_date: false, - is_time: false, - is_datetime: false, - is_duration: false, - is_period: false, - - ambiguous: false, +impl<'a> ParsedDateTime { + pub fn new() -> ParsedDateTime { + ParsedDateTime { year: 0, month: 1, day: 1, @@ -48,6 +48,47 @@ impl<'a> Parsed { microsecond: 0, offset: None, has_offset: false, + time_is_midnight: false, + } + } +} + +pub struct ParsedDuration { + pub years: i32, + pub months: i32, + pub days: i32, + pub hours: i32, + pub minutes: i32, + pub seconds: i32, + pub microseconds: i32, +} + +impl<'a> ParsedDuration { + pub fn new() -> ParsedDuration { + ParsedDuration { + years: 0, + months: 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<'a> Parsed { + pub fn new() -> Parsed { + Parsed { + datetime: None, + duration: None, + second_datetime: None, } } } @@ -93,16 +134,36 @@ impl<'a> Parser<'a> { } } - /// Increments the parser by `n` characters if the end of the input - /// has not been reached. Eliminates the need for repeated 'self.inc();' - /// in code. - fn inc_n(&mut self, n: usize) -> bool { - for _ in 0..n { - if !self.inc() { - return false; - } + fn parse_error(&mut self, message: String) -> ParseError { + ParseError { + index: self.idx, + c: self.current, + message: 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 { + "s" + } else { + "" + } + )); } - true + + self.parse_error(format!( + "Invalid character while parsing {}: {}.", + field_name, self.current, + )) } /// Returns true if the parser has reached the end of the input. @@ -110,385 +171,453 @@ impl<'a> Parser<'a> { self.idx >= self.src.len() } - pub fn parse(&mut self) -> Parsed { - let mut parsed = Parsed::new(); - parsed.is_date = true; + fn parse_integer(&mut self, length: usize, field_name: &str) -> Result { + let mut value: u32 = 0; - for _ in 0..4 { + for i in 0..length { if self.end() { - // TODO: Error + 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 self.current >= '0' && self.current <= '9' { - parsed.year = 10 * parsed.year + self.current.to_digit(10).unwrap() as u32; + value = 10 * value + self.current.to_digit(10).unwrap() as u32; self.inc(); } else { - // TODO: Error - return parsed; + return Err(self.unexpected_character_error(field_name, length - i)); } } - let mut leap = is_leap(parsed.year); - let mut separators: u32 = 0; - let mut week: u32 = 0; - let mut monthday: u32 = 0; - let mut i = 0; + Ok(value) + } - // Optional separator - if self.current == '-' { - separators += 1; - self.inc(); + pub fn parse(&mut self) -> Result { + let mut parsed = Parsed::new(); + + if self.current == 'P' { + // Duration (and possibly time interval) + } else { + self.parse_datetime(&mut parsed); } - // Checking for week dates - if self.current == 'W' { - self.inc(); + Ok(parsed) + } - let mut weekday: u32 = 1; + fn parse_datetime(&mut self, parsed: &mut Parsed) -> Result<(), ParseError> { + let mut datetime = ParsedDateTime::new(); + let mut extended_date_format: bool = false; - while !self.end() && self.current != ' ' && self.current != 'T' { - if self.current == '-' { - separators += 1; - self.inc(); - continue; - } + datetime.year = self.parse_integer(4, "year")?; - week = 10 * week + self.current.to_digit(10).unwrap() as u32; + if self.current == '-' { + self.inc(); + extended_date_format = true; - i += 1; + if self.current == 'W' { + // ISO week and day in extended format (i.e. Www-D) self.inc(); - } - match i { - 2 => (), // Only week number - 3 => { - // Week with week day - if !(separators == 0 || separators == 2) { - // We should have to or no separators - // TODO: Invalid week date - return parsed; - } - - weekday = week % 10; - week /= 10; - } - _ => { - // TODO: Invalid week date - return parsed; - } - } - - // Checks - if week > 53 || week > 52 && !is_long_year(parsed.year) { - // TODO: Invalid week number - return parsed; - } - - if weekday > 7 { - // TODO: Invalid week day - return parsed; - } + let iso_week = self.parse_integer(2, "iso week")?; + let mut iso_day: u32 = 1; - // Calculating ordinal day - let mut ordinal: u32 = week * 7 + weekday - (week_day(parsed.year, 1, 4) + 3); - - if ordinal < 1 { - // Previous year - ordinal += days_in_year(parsed.year - 1); - parsed.year -= 1; - leap = is_leap(parsed.year); - } + 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" + ))); + } - if ordinal > days_in_year(parsed.year) { - // Next year - ordinal -= days_in_year(parsed.year); - parsed.year += 1; - leap = is_leap(parsed.year); - } + self.inc(); - for j in 1..14 { - if ordinal <= MONTHS_OFFSETS[leap as usize][j] as u32 { - parsed.day = ordinal - MONTHS_OFFSETS[leap as usize][j - 1] as u32; - parsed.month = (j - 1) as u32; + iso_day = self.parse_integer(1, "iso day")?; + } - break; + 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 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; + + match self.ordinal_to_ymd(datetime.year, ordinal_day, false) { + Ok((year, month, day)) => { + datetime.year = year; + datetime.month = month; + datetime.day = day; + } + Err(error) => return Err(error), + } + } + } else { + datetime.day = 1; } } } else { - // At this point we need to check the number - // of characters until the end of the date part - // (or the end of the string). - // - // If two, we have only a month if there is a separator, it may be a time otherwise. - // If three, we have an ordinal date. - // If four, we have a complete date - while !self.end() && self.current != ' ' && self.current != 'T' { - if self.current == '-' { - separators += 1; - self.inc(); - continue; - } - - if !(self.current >= '0' && self.current <= '9') { - // TODO: Error - return parsed; - } - - monthday = 10 * monthday + self.current.to_digit(10).unwrap() as u32; + if self.current == 'W' { + // Compact ISO week and day (WwwD) self.inc(); - i += 1; - } - - match i { - 0 => (), - 2 => { - if separators == 0 { - // TODO: ambiguous - parsed.ambiguous = true; - (); - } + let iso_week = self.parse_integer(2, "iso week")?; + let mut iso_day: u32 = 1; - parsed.month = monthday; + if !self.end() && self.current != ' ' && self.current != 'T' { + iso_day = self.parse_integer(1, "iso day")?; } - 3 => { - // Ordinal day - if separators > 1 { - // TODO: Invalid ordinal date - return parsed; - } - if monthday < 1 || monthday > MONTHS_OFFSETS[leap as usize][13] as u32 { - // TODO: Invalid ordinal day for year - return parsed; + 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 + ordinal_day; - for j in 1..14 { - if monthday < MONTHS_OFFSETS[leap as usize][j] as u32 { - parsed.day = monthday - MONTHS_OFFSETS[leap as usize][j - 1] as u32; - parsed.month = (j - 1) as u32; - - break; + match self.ordinal_to_ymd(datetime.year, ordinal_day, false) { + Ok((year, month, day)) => { + datetime.year = year; + datetime.month = month; + datetime.day = day; } + Err(error) => return Err(error), } - } - 4 => { - // Month and day - parsed.month = monthday as u32 / 100; - parsed.day = monthday as u32 % 100; - } - _ => { - // TODO: Error - return parsed; + } else { + // Day + datetime.day = ordinal_day as u32 * 10 + self.parse_integer(1, "day")?; } } } - if separators > 0 && monthday == 0 && week == 0 { - // TODO: Invalid date + if !self.end() { + // Date/Time separator + if self.current != 'T' && self.current != ' ' { + return Err(self.parse_error(format!( + "Invalid character \"{}\" while parsing {}", + self.current, "date and time separator (\"T\" or \" \")" + ))); + } - return parsed; - } + self.inc(); - if parsed.month > 12 { - // TODO: Invalid month - return parsed; - } + // Hour + datetime.hour = self.parse_integer(2, "hour")?; - if parsed.day > DAYS_PER_MONTHS[leap as usize][parsed.month as usize] as u32 { - // TODO: Invalid day for month - return parsed; - } + 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(); - separators = 0; + // 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 (\":\")" + ))); + } - if self.current == 'T' || self.current == ' ' { - if parsed.ambiguous { - // TODO: Invalid date - return parsed; - } + 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 self.current >= '0' && self.current <= '9' { + datetime.microsecond = datetime.microsecond * 10 + + self.current.to_digit(10).unwrap(); + } 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 >= '0' && self.current <= '9' { + self.inc(); + } + + // Expand missing microsecond + while i < 6 { + datetime.microsecond *= 10; + i += 1; + } + } - // We have time so we have a datetime - parsed.is_date = false; - parsed.is_datetime = true; + if !extended_date_format { + return Err(self.parse_error(format!("Cannot combine \"basic\" date format with \"extended\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`)."))); + } + } + } 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 self.current >= '0' && self.current <= '9' { + datetime.microsecond = datetime.microsecond * 10 + + self.current.to_digit(10).unwrap(); + } 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 >= '0' && self.current <= '9' { + self.inc(); + } + + // Expand missing microsecond + while i < 6 { + datetime.microsecond *= 10; + i += 1; + } + } + } - if !self.inc() { - // TODO: invalid date - return parsed; + if extended_date_format { + return Err(self.parse_error(format!("Cannot combine \"extended\" date format with \"basic\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`)."))); + } + } } + } - // Grabbing time information - i = 0; - let mut time = 0; - while !self.end() - && self.current != '.' - && self.current != ',' - && self.current != 'Z' - && self.current != '+' - && self.current != '-' - { - if self.current == ':' { - separators += 1; - self.inc(); - continue; - } + 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 >= '0' && self.current <= '9') { - // TODO: Invalid time - return parsed; - } + if self.current == 'Z' || self.current == '+' || self.current == '-' { + // Optional timezone offset + let mut tzsign = 0; - time = 10 * time + self.current.to_digit(10).unwrap(); - i += 1; - self.inc(); + if self.current == '+' { + tzsign = 1; + } else if self.current == '-' { + tzsign = -1; } - match i { - 2 => { - // Hours only - if separators > 0 { - // Extraneous separators - // TODO: Invalid time - return parsed; - } + self.inc(); - parsed.hour = time; - } - 4 => { - // Hours and minutes - if separators > 1 { - // Extraneous separators - // TODO: Invalid time - return parsed; - } + let mut tzhour: i32 = 0; + let mut tzminute: i32 = 0; - parsed.hour = time / 100; - parsed.minute = time % 100; - } - 6 => { - // Hours, minutes and seconds - if separators > 2 { - // Extraneous separators - // TODO: Invalid time - return parsed; - } + if tzsign != 0 { + // Offset hour + tzhour = self.parse_integer(2, "timezone hour")? as i32; + if self.current == ':' { + // Optional separator + self.inc(); - parsed.hour = time / 10000; - parsed.minute = time / 100 % 100; - parsed.second = time % 100; - } - _ => { - // TODO: Invalid time - return parsed; + tzminute = self.parse_integer(2, "timezone minute")? as i32; + } else { + tzminute = self.parse_integer(2, "timezone minute")? as i32; } } - // Checks - if parsed.hour > 23 { - // TODO: Invalid hour - return parsed; + if tzminute > 59 { + return Err(self.parse_error(format!("timezone minute must be in 0..59"))); } - if parsed.minute > 59 { - // TODO: Invalid minute - return parsed; - } + tzminute += tzhour * 60; + tzminute *= tzsign; - if parsed.second > 59 { - // TODO: Invalid second - return parsed; + if tzminute.abs() > 1440 { + return Err(self.parse_error(format!("The absolute offset is to large"))); } - // Subsecond - if self.current == '.' || self.current == ',' { - self.inc(); + datetime.offset = Some(tzminute * 60); + } - time = 0; - i = 0; - while !self.end() - && self.current != 'Z' - && self.current != '+' - && self.current != '-' - { - if !(self.current >= '0' && self.current <= '9') { - // TODO: Invalid time - return parsed; - } + if !self.end() { + return Err(self.parse_error(format!("Unconverted data remains"))); + } - time = 10 * time + self.current.to_digit(10).unwrap(); - i += 1; - self.inc(); + match &parsed.datetime { + Some(_) => { + parsed.second_datetime = Some(datetime); + } + None => match &parsed.duration { + Some(_) => { + parsed.second_datetime = Some(datetime); } - - // Adjust to microseconds - if i > 6 { - parsed.microsecond = time / 10u32.pow(i - 6); - } else if i <= 6 { - parsed.microsecond = time * 10u32.pow(6 - i); + None => { + parsed.datetime = Some(datetime); } - } + }, } - // Timezone - if self.current == 'Z' { - parsed.has_offset = true; - self.inc(); - } else if self.current == '+' || self.current == '-' { - let tz_sign: i32 = if self.current == '+' { 1 } else { -1 }; + Ok(()) + } - parsed.has_offset = true; - self.inc(); + 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) { + return Err(ParseError { + index: self.idx, + c: self.current, + message: format!( + "Invalid ISO date: week {} out of range for year {}", + iso_week, iso_year + ), + }); + } - i = 0; - separators = 0; - let mut offset: i32 = 0; + if iso_day > 7 { + return Err(ParseError { + index: self.idx, + c: self.current, + message: format!("Invalid ISO date: week day is invalid"), + }); + } - while !self.end() { - if self.current == ':' { - separators += 1; - self.inc(); - continue; - } + let ordinal: i32 = + iso_week as i32 * 7 + iso_day as i32 - (week_day(iso_year, 1, 4) as i32 + 3); - if !(self.current >= '0' && self.current <= '9') { - // TODO: Invalid offset - return parsed; - } + self.ordinal_to_ymd(iso_year, ordinal, true) + } - offset = 10 * offset + self.current.to_digit(10).unwrap() as i32; - self.inc(); - i += 1; + 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 = is_leap(y) as usize; + let mut month: u32 = 1; + let mut day: u32 = 1; + + if ord < 1 { + if !allow_out_of_bounds { + return Err(self.parse_error(format!( + "Invalid ordinal day: {} is too small for year {}", + ordinal.to_string(), + year.to_string() + ))); } + // Previous year + ord += days_in_year(year - 1) as i32; + y -= 1; + leap = is_leap(y) as usize; + } - match i { - 2 => { - // hh format - if separators > 0 { - // Extraneous separators - // TODO: Invalid offset - return parsed; - } + if ord > days_in_year(y) as i32 { + if !allow_out_of_bounds { + return Err(self.parse_error(format!( + "Invalid ordinal day: {} is too large for year {}", + ordinal.to_string(), + year.to_string() + ))); + } - parsed.offset = Some(tz_sign * offset * 3600); - } - 4 => { - // hhmm format - if separators > 1 { - // Extraneous separators - // TODO: Invalid offset - return parsed; - } + // Next year + ord -= days_in_year(y) as i32; + y += 1; + leap = is_leap(y) as usize; + } - parsed.offset = Some(tz_sign * ((offset / 100 * 3600) + (offset % 100 * 60))); - } - _ => { - // Wrong format - // TODO: Invalid format - return parsed; - } + for i in 1..14 { + if ord < MONTHS_OFFSETS[leap][i] { + day = ord as u32 - MONTHS_OFFSETS[leap][i - 1] as u32; + month = (i - 1) as u32; + + return Ok((y as u32, month, day)); } } - return parsed; + Err(self.parse_error(format!( + "Invalid ordinal day: {} is too large for year {}", + ordinal.to_string(), + year.to_string() + ))) } } diff --git a/rust/python/parsing.rs b/rust/python/parsing.rs index 3e44fe34..6542f70f 100644 --- a/rust/python/parsing.rs +++ b/rust/python/parsing.rs @@ -1,38 +1,53 @@ +use pyo3::exceptions; use pyo3::{prelude::*, types::PyDateTime}; use crate::parsing::Parser; use crate::python::types::FixedTimezone; #[pyfunction] -pub fn parse_iso8601<'p>(py: Python<'p>, input: &str) -> PyResult<&'p PyDateTime> { +pub fn parse_iso8601(py: Python, input: &str) -> PyResult { let parsed = Parser::new(input).parse(); - match parsed.offset { - Some(offset) => PyDateTime::new( - py, - parsed.year as i32, - parsed.month as u8, - parsed.day as u8, - parsed.hour as u8, - parsed.minute as u8, - parsed.second as u8, - parsed.microsecond as u32, - Some( - Py::new(py, FixedTimezone::new(offset, None))? - .to_object(py) - .extract(py)?, - ), - ), - None => PyDateTime::new( - py, - parsed.year as i32, - parsed.month as u8, - parsed.day as u8, - parsed.hour as u8, - parsed.minute as u8, - parsed.second as u8, - parsed.microsecond as u32, - None, - ), + match parsed { + Ok(parsed) => match (parsed.datetime, parsed.duration, parsed.second_datetime) { + (Some(datetime), None, None) => 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 as u32, + Some( + Py::new(py, FixedTimezone::new(offset, None))? + .to_object(py) + .extract(py)?, + ), + )?; + + return 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 as u32, + None, + )?; + + return Ok(dt.to_object(py)); + } + }, + (_, _, _) => todo!(), + }, + Err(error) => Err(exceptions::PyValueError::new_err(format!("{}", error))), } } From 2f148a381275e8abaff667e0161e4118e568b5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Thu, 15 Jun 2023 00:58:59 +0200 Subject: [PATCH 04/19] Implement duration parsing in Rust --- pendulum/parser.py | 6 +- pendulum/parsing/__init__.py | 5 +- rust/parsing.rs | 241 +++++++++++++- rust/python/mod.rs | 2 + rust/python/parsing.rs | 18 +- rust/python/types/duration.rs | 58 ++++ rust/python/types/mod.rs | 2 + tests/parsing/test_parse_iso8601.py | 473 ++++++---------------------- 8 files changed, 419 insertions(+), 386 deletions(-) create mode 100644 rust/python/types/duration.rs diff --git a/pendulum/parser.py b/pendulum/parser.py index d39842f6..c364789b 100644 --- a/pendulum/parser.py +++ b/pendulum/parser.py @@ -18,9 +18,9 @@ from pendulum.time import Time try: - from pendulum.parsing._iso8601 import Duration as CDuration + from _pendulum import Duration as CDuration except ImportError: - CDuration = None # type: ignore[misc, assignment] + CDuration = None def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration: @@ -122,4 +122,4 @@ def _parse(text: str, **options: t.Any) -> Date | DateTime | Time | Duration | I microseconds=parsed.microseconds, ) - return parsed + raise NotImplementedError diff --git a/pendulum/parsing/__init__.py b/pendulum/parsing/__init__.py index 3b64994a..908b670d 100644 --- a/pendulum/parsing/__init__.py +++ b/pendulum/parsing/__init__.py @@ -24,12 +24,13 @@ if not with_extensions or struct.calcsize("P") == 4: raise ImportError() - from pendulum.parsing._iso8601 import Duration - from pendulum.parsing._iso8601 import parse_iso8601 + from _pendulum import Duration + from _pendulum import parse_iso8601 except ImportError: from pendulum.duration import Duration # type: ignore[assignment] from pendulum.parsing.iso8601 import parse_iso8601 # type: ignore[assignment] + COMMON = re.compile( # Date (optional) # noqa: ERA001 "^" diff --git a/rust/parsing.rs b/rust/parsing.rs index f57b5257..f14ae9d9 100644 --- a/rust/parsing.rs +++ b/rust/parsing.rs @@ -54,13 +54,14 @@ impl<'a> ParsedDateTime { } pub struct ParsedDuration { - pub years: i32, - pub months: i32, - pub days: i32, - pub hours: i32, - pub minutes: i32, - pub seconds: i32, - pub microseconds: i32, + 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<'a> ParsedDuration { @@ -68,6 +69,7 @@ impl<'a> ParsedDuration { ParsedDuration { years: 0, months: 0, + weeks: 0, days: 0, hours: 0, minutes: 0, @@ -200,8 +202,9 @@ impl<'a> Parser<'a> { if self.current == 'P' { // Duration (and possibly time interval) + self.parse_duration(&mut parsed)?; } else { - self.parse_datetime(&mut parsed); + self.parse_datetime(&mut parsed)?; } Ok(parsed) @@ -493,7 +496,7 @@ impl<'a> Parser<'a> { self.inc(); tzminute = self.parse_integer(2, "timezone minute")? as i32; - } else { + } else if !self.end() { tzminute = self.parse_integer(2, "timezone minute")? as i32; } } @@ -513,6 +516,22 @@ impl<'a> Parser<'a> { } 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(format!("Unconverted data remains"))); } @@ -533,6 +552,210 @@ impl<'a> Parser<'a> { 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(format!("Repeated time declaration in duration")) + ); + } + + got_t = true; + } + _c => { + let (value, op_fraction) = self.parse_duration_number_frac()?; + if last_had_fraction { + return Err(self.parse_error(format!("Invalid duration fraction"))); + } + + if op_fraction.is_some() { + last_had_fraction = true; + } + + if got_t { + match self.current { + 'H' => { + duration.hours += value; + + if let Some(fraction) = op_fraction { + let extra_minutes = fraction * 60 as 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 = duration.seconds + extra_full_seconds as u32; + let micro_extra = ((extra_seconds - extra_full_seconds) + * 1_000_000.0) + .round() + as u32; + duration.microseconds = duration.microseconds + micro_extra; + } + } + 'M' => { + duration.minutes += value; + + if let Some(fraction) = op_fraction { + let extra_seconds = fraction * 60 as f64; + let extra_full_seconds = extra_seconds.trunc(); + duration.seconds = duration.seconds + extra_full_seconds as u32; + let micro_extra = ((extra_seconds - extra_full_seconds) + * 1_000_000.0) + .round() + as u32; + duration.microseconds = 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(format!("Invalid duration time unit"))) + } + } + } else { + match self.current { + 'Y' => { + if last_had_fraction { + return Err(self.parse_error(format!( + "Fractional years in duration are not supported" + ))); + } + + duration.years = value; + } + 'M' => { + if last_had_fraction { + return Err(self.parse_error(format!( + "Fractional months in duration are not supported" + ))); + } + + duration.months = value; + } + 'W' => { + duration.weeks = value; + + if let Some(fraction) = op_fraction { + let extra_days = fraction * 7 as f64; + let extra_full_days = extra_days.trunc(); + duration.days = 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 = duration.seconds + extra_full_seconds as u32; + let micro_extra = ((extra_seconds - extra_full_seconds) + * 1_000_000.0) + .round() + as u32; + duration.microseconds = duration.microseconds + micro_extra; + } + } + 'D' => { + 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 = duration.seconds + extra_full_seconds as u32; + let micro_extra = ((extra_seconds - extra_full_seconds) + * 1_000_000.0) + .round() + as u32; + duration.microseconds = duration.microseconds + micro_extra; + } + } + _ => { + return Err(self.parse_error(format!("Invalid duration time unit"))) + } + } + } + } + _ => break, + } + self.inc(); + + if self.end() { + break; + } + } + + parsed.duration = Some(duration); + + return Ok(()); + } + + fn parse_duration_number_frac(&mut self) -> Result<(u32, Option), ParseError> { + let value = self.parse_duration_number()?; + if self.current == '.' || self.current == ',' { + let mut decimal = 0_f64; + let mut denominator = 1_f64; + loop { + self.inc(); + + match self.current { + c if c.is_ascii_digit() => { + decimal *= 10.0; + decimal += c.to_digit(10).unwrap() as f64; + denominator *= 10.0; + } + _ => return Ok((value, Some(decimal / denominator))), + } + } + } else { + Ok((value, None)) + } + } + + fn parse_duration_number(&mut self) -> Result { + let mut value = match self.current { + c if c.is_ascii_digit() => c.to_digit(10).unwrap() as u32, + _ => { + return Err(self.parse_error(format!("Invalid number in duration"))); + } + }; + + loop { + self.inc(); + + match self.current { + c if c.is_ascii_digit() => { + value = value * 10; + value = value + c.to_digit(10).unwrap() as u32; + } + _ => return Ok(value), + } + } + } + fn iso_to_ymd( &mut self, iso_year: u32, diff --git a/rust/python/mod.rs b/rust/python/mod.rs index c67e3fae..e1b34b45 100644 --- a/rust/python/mod.rs +++ b/rust/python/mod.rs @@ -6,6 +6,7 @@ mod types; use helpers::{is_leap, is_long_year, local_time, week_day}; use parsing::parse_iso8601; +use types::Duration; #[pymodule] pub fn _pendulum(_py: Python<'_>, m: &PyModule) -> PyResult<()> { @@ -14,6 +15,7 @@ pub fn _pendulum(_py: Python<'_>, m: &PyModule) -> PyResult<()> { 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_class::()?; Ok(()) } diff --git a/rust/python/parsing.rs b/rust/python/parsing.rs index 6542f70f..60505f06 100644 --- a/rust/python/parsing.rs +++ b/rust/python/parsing.rs @@ -2,7 +2,7 @@ use pyo3::exceptions; use pyo3::{prelude::*, types::PyDateTime}; use crate::parsing::Parser; -use crate::python::types::FixedTimezone; +use crate::python::types::{Duration, FixedTimezone}; #[pyfunction] pub fn parse_iso8601(py: Python, input: &str) -> PyResult { @@ -46,6 +46,22 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { return Ok(dt.to_object(py)); } }, + (None, Some(duration), None) => { + return 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)); + } (_, _, _) => todo!(), }, Err(error) => Err(exceptions::PyValueError::new_err(format!("{}", error))), diff --git a/rust/python/types/duration.rs b/rust/python/types/duration.rs new file mode 100644 index 00000000..fca4fe73 --- /dev/null +++ b/rust/python/types/duration.rs @@ -0,0 +1,58 @@ +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))] + 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/python/types/mod.rs b/rust/python/types/mod.rs index aac0dad4..236f6e37 100644 --- a/rust/python/types/mod.rs +++ b/rust/python/types/mod.rs @@ -1,3 +1,5 @@ +mod duration; mod timezone; +pub use duration::Duration; pub use timezone::FixedTimezone; diff --git a/tests/parsing/test_parse_iso8601.py b/tests/parsing/test_parse_iso8601.py index 83f28810..455234ea 100644 --- a/tests/parsing/test_parse_iso8601.py +++ b/tests/parsing/test_parse_iso8601.py @@ -2,7 +2,6 @@ from datetime import date from datetime import datetime -from datetime import time import pytest @@ -10,82 +9,72 @@ try: - from pendulum.parsing._extension import TZFixedOffset as FixedTimezone + from _pendulum import FixedTimezone except ImportError: from pendulum.tz.timezone import FixedTimezone -def test_parse_iso8601(): - # Date - assert date(2016, 1, 1) == parse_iso8601("2016") - assert date(2016, 10, 1) == parse_iso8601("2016-10") - assert date(2016, 10, 6) == parse_iso8601("2016-10-06") - assert date(2016, 10, 6) == parse_iso8601("20161006") - - # Time - assert time(20, 16, 10, 0) == parse_iso8601("201610") - - # Datetime - assert datetime(2016, 10, 6, 12, 34, 56, 123456) == parse_iso8601( - "2016-10-06T12:34:56.123456" - ) - assert datetime(2016, 10, 6, 12, 34, 56, 123000) == parse_iso8601( - "2016-10-06T12:34:56.123" - ) - assert datetime(2016, 10, 6, 12, 34, 56, 123) == parse_iso8601( - "2016-10-06T12:34:56.000123" - ) - assert datetime(2016, 10, 6, 12, 0, 0, 0) == parse_iso8601("2016-10-06T12") - assert datetime(2016, 10, 6, 12, 34, 56, 0) == parse_iso8601("2016-10-06T123456") - assert datetime(2016, 10, 6, 12, 34, 56, 123456) == parse_iso8601( - "2016-10-06T123456.123456" - ) - assert datetime(2016, 10, 6, 12, 34, 56, 123456) == parse_iso8601( - "20161006T123456.123456" - ) - assert datetime(2016, 10, 6, 12, 34, 56, 123456) == parse_iso8601( - "20161006 123456.123456" - ) - - # Datetime with offset - assert datetime( - 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(19800) - ) == parse_iso8601("2016-10-06T12:34:56.123456+05:30") - assert datetime( - 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(19800) - ) == parse_iso8601("2016-10-06T12:34:56.123456+0530") - assert datetime( - 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-19800) - ) == parse_iso8601("2016-10-06T12:34:56.123456-05:30") - assert datetime( - 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-19800) - ) == parse_iso8601("2016-10-06T12:34:56.123456-0530") - assert datetime( - 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(18000) - ) == parse_iso8601("2016-10-06T12:34:56.123456+05") - assert datetime( - 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-18000) - ) == parse_iso8601("2016-10-06T12:34:56.123456-05") - assert datetime( - 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-18000) - ) == parse_iso8601("20161006T123456,123456-05") - assert datetime( - 2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(+19800) - ) == parse_iso8601("2016-10-06T12:34:56.123456789+05:30") - - # Ordinal date - assert date(2012, 1, 7) == parse_iso8601("2012-007") - assert date(2012, 1, 7) == parse_iso8601("2012007") - assert date(2017, 3, 20) == parse_iso8601("2017-079") - - # Week date - assert date(2012, 1, 30) == parse_iso8601("2012-W05") - assert date(2008, 9, 27) == parse_iso8601("2008-W39-6") - assert date(2010, 1, 3) == parse_iso8601("2009-W53-7") - assert date(2008, 12, 29) == parse_iso8601("2009-W01-1") - - # Week date wth time - assert datetime(2008, 9, 27, 9, 0, 0, 0) == parse_iso8601("2008-W39-6T09") +@pytest.mark.parametrize( + ["text", "expected"], + [ + ("2016-10", datetime(2016, 10, 1)), + ("2016-10-06", datetime(2016, 10, 6)), + # Ordinal date + ("2012-007", datetime(2012, 1, 7)), + ("2012007", datetime(2012, 1, 7)), + ("2017-079", datetime(2017, 3, 20)), + # Week date + ("2012-W05", datetime(2012, 1, 30)), + ("2008-W39-6", datetime(2008, 9, 27)), + ("2009-W53-7", datetime(2010, 1, 3)), + ("2009-W01-1", datetime(2008, 12, 29)), + # Datetime + ("2016-10-06T12:34:56.123456", datetime(2016, 10, 6, 12, 34, 56, 123456)), + ("2016-10-06T12:34:56.123", datetime(2016, 10, 6, 12, 34, 56, 123000)), + ("2016-10-06T12:34:56.000123", datetime(2016, 10, 6, 12, 34, 56, 123)), + ("20161006T12", datetime(2016, 10, 6, 12, 0, 0, 0)), + ("20161006T123456", datetime(2016, 10, 6, 12, 34, 56, 0)), + ("20161006T123456.123456", datetime(2016, 10, 6, 12, 34, 56, 123456)), + ("20161006 123456.123456", datetime(2016, 10, 6, 12, 34, 56, 123456)), + # Datetime with offset + ( + "2016-10-06T12:34:56.123456+05:30", + datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(19800)), + ), + ( + "2016-10-06T12:34:56.123456+0530", + datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(19800)), + ), + ( + "2016-10-06T12:34:56.123456-05:30", + datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-19800)), + ), + ( + "2016-10-06T12:34:56.123456-0530", + datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-19800)), + ), + ( + "2016-10-06T12:34:56.123456+05", + datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(18000)), + ), + ( + "2016-10-06T12:34:56.123456-05", + datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-18000)), + ), + ( + "20161006T123456,123456-05", + datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(-18000)), + ), + ( + "2016-10-06T12:34:56.123456789+05:30", + datetime(2016, 10, 6, 12, 34, 56, 123456, FixedTimezone(+19800)), + ), + # Week date with time + ("2008-W39-6T09", datetime(2008, 9, 27, 9, 0, 0, 0)), + ], +) +def test_parse_iso8601(text: str, expected: date) -> None: + assert parse_iso8601(text) == expected def test_parse_ios8601_invalid(): @@ -166,301 +155,43 @@ def test_parse_ios8601_invalid(): parse_iso8601("2012-W123") # Missing separator -def test_parse_ios8601_duration(): - text = "P2Y3M4DT5H6M7S" - parsed = parse_iso8601(text) - - assert parsed.years == 2 - assert parsed.months == 3 - assert parsed.weeks == 0 - assert parsed.remaining_days == 4 - assert parsed.hours == 5 - assert parsed.minutes == 6 - assert parsed.remaining_seconds == 7 - assert parsed.microseconds == 0 - - text = "P1Y2M3DT4H5M6.5S" - parsed = parse_iso8601(text) - - assert parsed.years == 1 - assert parsed.months == 2 - assert parsed.weeks == 0 - assert parsed.remaining_days == 3 - assert parsed.hours == 4 - assert parsed.minutes == 5 - assert parsed.remaining_seconds == 6 - assert parsed.microseconds == 500000 - - text = "P1Y2M3DT4H5M6,5S" - parsed = parse_iso8601(text) - - assert parsed.years == 1 - assert parsed.months == 2 - assert parsed.weeks == 0 - assert parsed.remaining_days == 3 - assert parsed.hours == 4 - assert parsed.minutes == 5 - assert parsed.remaining_seconds == 6 - assert parsed.microseconds == 500000 - - text = "P1Y2M3D" - parsed = parse_iso8601(text) - - assert parsed.years == 1 - assert parsed.months == 2 - assert parsed.weeks == 0 - assert parsed.remaining_days == 3 - assert parsed.hours == 0 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "P1Y2M3.5D" - parsed = parse_iso8601(text) - - assert parsed.years == 1 - assert parsed.months == 2 - assert parsed.weeks == 0 - assert parsed.remaining_days == 3 - assert parsed.hours == 12 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "P1Y2M3,5D" - parsed = parse_iso8601(text) - - assert parsed.years == 1 - assert parsed.months == 2 - assert parsed.weeks == 0 - assert parsed.remaining_days == 3 - assert parsed.hours == 12 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "PT4H54M6.5S" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 0 - assert parsed.weeks == 0 - assert parsed.remaining_days == 0 - assert parsed.hours == 4 - assert parsed.minutes == 54 - assert parsed.remaining_seconds == 6 - assert parsed.microseconds == 500000 - - text = "PT4H54M6,5S" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 0 - assert parsed.weeks == 0 - assert parsed.remaining_days == 0 - assert parsed.hours == 4 - assert parsed.minutes == 54 - assert parsed.remaining_seconds == 6 - assert parsed.microseconds == 500000 - - text = "P1Y" - parsed = parse_iso8601(text) - - assert parsed.years == 1 - assert parsed.months == 0 - assert parsed.weeks == 0 - assert parsed.remaining_days == 0 - assert parsed.hours == 0 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "P1.5Y" - with pytest.raises(ValueError): - parse_iso8601(text) - - text = "P1,5Y" - with pytest.raises(ValueError): - parse_iso8601(text) - - text = "P1M" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 1 - assert parsed.weeks == 0 - assert parsed.remaining_days == 0 - assert parsed.hours == 0 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "P1.5M" - with pytest.raises(ValueError): - parse_iso8601(text) - - text = "P1,5M" - with pytest.raises(ValueError): - parse_iso8601(text) - - text = "P1W" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 0 - assert parsed.weeks == 1 - assert parsed.remaining_days == 0 - assert parsed.hours == 0 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "P1.5W" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 0 - assert parsed.weeks == 1 - assert parsed.remaining_days == 3 - assert parsed.hours == 12 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "P1,5W" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 0 - assert parsed.weeks == 1 - assert parsed.remaining_days == 3 - assert parsed.hours == 12 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "P1D" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 0 - assert parsed.weeks == 0 - assert parsed.remaining_days == 1 - assert parsed.hours == 0 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "P1.5D" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 0 - assert parsed.weeks == 0 - assert parsed.remaining_days == 1 - assert parsed.hours == 12 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "P1,5D" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 0 - assert parsed.weeks == 0 - assert parsed.remaining_days == 1 - assert parsed.hours == 12 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "PT1H" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 0 - assert parsed.weeks == 0 - assert parsed.remaining_days == 0 - assert parsed.hours == 1 - assert parsed.minutes == 0 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "PT1.5H" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 0 - assert parsed.weeks == 0 - assert parsed.remaining_days == 0 - assert parsed.hours == 1 - assert parsed.minutes == 30 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - text = "PT1,5H" - parsed = parse_iso8601(text) - - assert parsed.years == 0 - assert parsed.months == 0 - assert parsed.weeks == 0 - assert parsed.remaining_days == 0 - assert parsed.hours == 1 - assert parsed.minutes == 30 - assert parsed.remaining_seconds == 0 - assert parsed.microseconds == 0 - - # Double digit with 0 - text = "P2Y30M4DT5H6M7S" - parsed = parse_iso8601(text) - - assert parsed.years == 2 - assert parsed.months == 30 - assert parsed.weeks == 0 - assert parsed.remaining_days == 4 - assert parsed.hours == 5 - assert parsed.minutes == 6 - assert parsed.remaining_seconds == 7 - assert parsed.microseconds == 0 - - # No P operator - with pytest.raises(ValueError): - parse_iso8601("2Y3M4DT5H6M7S") - - # Week and other units combined - with pytest.raises(ValueError): - parse_iso8601("P1Y2W") - - # Invalid units order - with pytest.raises(ValueError): - parse_iso8601("P1S") - - with pytest.raises(ValueError): - parse_iso8601("P1D1S") - - with pytest.raises(ValueError): - parse_iso8601("1Y2M3D1SPT1M") - - with pytest.raises(ValueError): - parse_iso8601("P1Y2M3D2MT1S") - - with pytest.raises(ValueError): - parse_iso8601("P2M3D1ST1Y1M") - - with pytest.raises(ValueError): - parse_iso8601("P1Y2M2MT3D1S") - - with pytest.raises(ValueError): - parse_iso8601("P1D1Y1M") - - with pytest.raises(ValueError): - parse_iso8601("PT1S1H") - - # Invalid - with pytest.raises(ValueError): - parse_iso8601("P1Dasdfasdf") - - # Invalid fractional - with pytest.raises(ValueError): - parse_iso8601("P2Y3M4DT5.5H6M7S") +@pytest.mark.parametrize( + ["text", "expected"], + [ + ("P2Y3M4DT5H6M7S", (2, 3, 0, 4, 5, 6, 7, 0)), + ("P1Y2M3DT4H5M6.5S", (1, 2, 0, 3, 4, 5, 6, 500_000)), + ("P1Y2M3DT4H5M6,5S", (1, 2, 0, 3, 4, 5, 6, 500_000)), + ("P1Y2M3D", (1, 2, 0, 3, 0, 0, 0, 0)), + ("P1Y2M3.5D", (1, 2, 0, 3, 12, 0, 0, 0)), + ("P1Y2M3,5D", (1, 2, 0, 3, 12, 0, 0, 0)), + ("PT4H54M6.5S", (0, 0, 0, 0, 4, 54, 6, 500_000)), + ("PT4H54M6,5S", (0, 0, 0, 0, 4, 54, 6, 500_000)), + ("P1Y", (1, 0, 0, 0, 0, 0, 0, 0)), + ("P1M", (0, 1, 0, 0, 0, 0, 0, 0)), + ("P1W", (0, 0, 1, 0, 0, 0, 0, 0)), + ("P1.5W", (0, 0, 1, 3, 12, 0, 0, 0)), + ("P1,5W", (0, 0, 1, 3, 12, 0, 0, 0)), + ("P1D", (0, 0, 0, 1, 0, 0, 0, 0)), + ("P1.5D", (0, 0, 0, 1, 12, 0, 0, 0)), + ("P1,5D", (0, 0, 0, 1, 12, 0, 0, 0)), + ("PT1H", (0, 0, 0, 0, 1, 0, 0, 0)), + ("PT1.5H", (0, 0, 0, 0, 1, 30, 0, 0)), + ("PT1,5H", (0, 0, 0, 0, 1, 30, 0, 0)), + ("P2Y30M4DT5H6M7S", (2, 30, 0, 4, 5, 6, 7, 0)), + ], +) +def test_parse_ios8601_duration( + text: str, expected: tuple[int, int, int, int, int, int, int, int] +) -> None: + parsed = parse_iso8601(text) + + assert ( + parsed.years, + parsed.months, + parsed.weeks, + parsed.remaining_days, + parsed.hours, + parsed.minutes, + parsed.remaining_seconds, + parsed.microseconds, + ) == expected From 6b4468a0c538988f80e4277022dfc694d67f42e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Mon, 19 Jun 2023 10:22:40 +0200 Subject: [PATCH 05/19] Reintroduce type accuracy parsing --- Cargo.lock | 24 +- Cargo.toml | 2 +- rust/parsing.rs | 342 +++++++++++++++++----------- rust/python/parsing.rs | 102 ++++++--- rust/python/types/timezone.rs | 13 +- tests/parsing/test_parse_iso8601.py | 31 ++- 6 files changed, 323 insertions(+), 191 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a067ca16..42aa1cc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,9 +59,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a3d8e8a46ab2738109347433cb7b96dffda2e4a218b03ef27090238886b147" +checksum = "cffef52f74ec3b1a1baf295d9b8fcc3070327aefc39a6d00656b13c1d0b8885c" dependencies = [ "cfg-if", "indoc", @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75439f995d07ddfad42b192dfcf3bc66a7ecfd8b4a1f5f6f046aa5c2c5d7677d" +checksum = "713eccf888fb05f1a96eb78c0dbc51907fee42b3377272dc902eb38985f418d5" dependencies = [ "once_cell", "target-lexicon", @@ -158,9 +158,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839526a5c07a17ff44823679b68add4a58004de00512a95b6c1c98a6dcac0ee5" +checksum = "5b2ecbdcfb01cbbf56e179ce969a048fd7305a66d4cdf3303e0da09d69afe4c3" dependencies = [ "libc", "pyo3-build-config", @@ -168,9 +168,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd44cf207476c6a9760c4653559be4f206efafb924d3e4cbf2721475fc0d6cc5" +checksum = "b78fdc0899f2ea781c463679b20cb08af9247febc8d052de941951024cd8aea0" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -180,9 +180,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1f43d8e30460f36350d18631ccf85ded64c059829208fe680904c65bcd0a4c" +checksum = "60da7b84f1227c3e2fe7593505de274dcf4c8928b4e0a1c23d551a14e4e80a0f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7d3b0fd0..737ec459 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ path = "rust/lib.rs" [dependencies] iso8601 = "0.6.1" nom = "7.1.3" -pyo3 = { version = "0.18.1", features = ["extension-module"] } +pyo3 = { version = "0.19.0", features = ["extension-module"] } [features] extension-module = ["pyo3/extension-module"] diff --git a/rust/parsing.rs b/rust/parsing.rs index f14ae9d9..2af5b976 100644 --- a/rust/parsing.rs +++ b/rust/parsing.rs @@ -33,6 +33,9 @@ pub struct ParsedDateTime { pub microsecond: u32, pub offset: Option, pub has_offset: bool, + pub has_date: bool, + pub has_time: bool, + pub extended_date_format: bool, pub time_is_midnight: bool, } @@ -48,6 +51,9 @@ impl<'a> ParsedDateTime { microsecond: 0, offset: None, has_offset: false, + has_date: false, + has_time: false, + extended_date_format: false, time_is_midnight: false, } } @@ -212,13 +218,67 @@ impl<'a> Parser<'a> { fn parse_datetime(&mut self, parsed: &mut Parsed) -> Result<(), ParseError> { let mut datetime = ParsedDateTime::new(); - let mut extended_date_format: bool = false; - datetime.year = self.parse_integer(4, "year")?; + if self.current == 'T' { + self.parse_time(&mut datetime, false)?; + + if !self.end() { + return Err(self.parse_error(format!("Unconverted data remains"))); + } + + 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(format!("Unconverted data remains"))); + } + + 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(); - extended_date_format = true; + datetime.extended_date_format = true; if self.current == 'W' { // ISO week and day in extended format (i.e. Www-D) @@ -327,138 +387,180 @@ impl<'a> Parser<'a> { } if !self.end() { - // Date/Time separator - if self.current != 'T' && self.current != ' ' { - return Err(self.parse_error(format!( - "Invalid character \"{}\" while parsing {}", - self.current, "date and time separator (\"T\" or \" \")" - ))); + 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(format!("Unconverted data remains"))); + } + + 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> { + // 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(); + 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 (\":\")" - ))); - } + // Minute + datetime.minute = self.parse_integer(2, "minute")?; - self.inc(); + 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 (\":\")" + ))); + } - // Second - datetime.second = self.parse_integer(2, "second")?; + self.inc(); - if self.current == '.' || self.current == ',' { - // Optional fractional second - self.inc(); + // Second + datetime.second = self.parse_integer(2, "second")?; - datetime.microsecond = 0; - let mut i: u8 = 0; - - while i < 6 { - if self.current >= '0' && self.current <= '9' { - datetime.microsecond = datetime.microsecond * 10 - + self.current.to_digit(10).unwrap(); - } else if i == 0 { - // One digit minimum is required - return Err(self.unexpected_character_error("subsecond", 1)); - } else { - break; - } + if self.current == '.' || self.current == ',' { + // Optional fractional second + self.inc(); - self.inc(); - i += 1; + datetime.microsecond = 0; + let mut i: u8 = 0; + + while i < 6 { + if self.current >= '0' && self.current <= '9' { + datetime.microsecond = + datetime.microsecond * 10 + self.current.to_digit(10).unwrap(); + } else if i == 0 { + // One digit minimum is required + return Err(self.unexpected_character_error("subsecond", 1)); + } else { + break; } - // Drop extraneous digits - while self.current >= '0' && self.current <= '9' { - self.inc(); - } + self.inc(); + i += 1; + } - // Expand missing microsecond - while i < 6 { - datetime.microsecond *= 10; - i += 1; - } + // Drop extraneous digits + while self.current >= '0' && self.current <= '9' { + self.inc(); } - if !extended_date_format { - return Err(self.parse_error(format!("Cannot combine \"basic\" date format with \"extended\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`)."))); + // Expand missing microsecond + while i < 6 { + datetime.microsecond *= 10; + i += 1; } } - } else { - // Minute and second in compact format (mmss) - // Minute - datetime.minute = self.parse_integer(2, "minute")?; + if !datetime.extended_date_format { + return Err(self.parse_error(format!("Cannot combine \"basic\" date format with \"extended\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`)."))); + } + } + } else { + // Minute and second in compact format (mmss) - if !self.end() - && self.current != 'Z' - && self.current != '+' - && self.current != '-' - { - // Optional second + // Minute + datetime.minute = self.parse_integer(2, "minute")?; - datetime.second = self.parse_integer(2, "second")?; + if !self.end() && self.current != 'Z' && self.current != '+' && self.current != '-' + { + // Optional second - if self.current == '.' || self.current == ',' { - // Optional fractional second - self.inc(); + datetime.second = self.parse_integer(2, "second")?; - datetime.microsecond = 0; - let mut i: u8 = 0; - - while i < 6 { - if self.current >= '0' && self.current <= '9' { - datetime.microsecond = datetime.microsecond * 10 - + self.current.to_digit(10).unwrap(); - } else if i == 0 { - // One digit minimum is required - return Err(self.unexpected_character_error("subsecond", 1)); - } else { - break; - } + if self.current == '.' || self.current == ',' { + // Optional fractional second + self.inc(); - self.inc(); - i += 1; + datetime.microsecond = 0; + let mut i: u8 = 0; + + while i < 6 { + if self.current >= '0' && self.current <= '9' { + datetime.microsecond = + datetime.microsecond * 10 + self.current.to_digit(10).unwrap(); + } else if i == 0 { + // One digit minimum is required + return Err(self.unexpected_character_error("subsecond", 1)); + } else { + break; } - // Drop extraneous digits - while self.current >= '0' && self.current <= '9' { - self.inc(); - } + self.inc(); + i += 1; + } - // Expand missing microsecond - while i < 6 { - datetime.microsecond *= 10; - i += 1; - } + // Drop extraneous digits + while self.current >= '0' && self.current <= '9' { + self.inc(); } - } - if extended_date_format { - return Err(self.parse_error(format!("Cannot combine \"extended\" date format with \"basic\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`)."))); + // Expand missing microsecond + while i < 6 { + datetime.microsecond *= 10; + i += 1; + } } } + + if datetime.extended_date_format { + return Err(self.parse_error(format!("Cannot combine \"extended\" date format with \"basic\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`)."))); + } } } @@ -515,41 +617,7 @@ impl<'a> Parser<'a> { datetime.offset = Some(tzminute * 60); } - 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(format!("Unconverted data remains"))); - } - - 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(()) + return Ok(()); } fn parse_duration(&mut self, parsed: &mut Parsed) -> Result<(), ParseError> { diff --git a/rust/python/parsing.rs b/rust/python/parsing.rs index 60505f06..16113e86 100644 --- a/rust/python/parsing.rs +++ b/rust/python/parsing.rs @@ -1,7 +1,10 @@ use pyo3::exceptions; -use pyo3::{prelude::*, types::PyDateTime}; +use pyo3::prelude::*; +use pyo3::types::PyDate; +use pyo3::types::PyDateTime; +use pyo3::types::PyTime; -use crate::parsing::Parser; +use crate::parsing::{ParseError, Parser}; use crate::python::types::{Duration, FixedTimezone}; #[pyfunction] @@ -10,41 +13,84 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { match parsed { Ok(parsed) => match (parsed.datetime, parsed.duration, parsed.second_datetime) { - (Some(datetime), None, None) => 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 as u32, - Some( - Py::new(py, FixedTimezone::new(offset, None))? - .to_object(py) - .extract(py)?, - ), - )?; + (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 as u32, + Some( + Py::new(py, FixedTimezone::new(offset, None))? + .to_object(py) + .extract(py)?, + ), + )?; - return Ok(dt.to_object(py)); - } - None => { - let dt = PyDateTime::new( + return 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 as u32, + None, + )?; + + return Ok(dt.to_object(py)); + } + }, + (true, false) => { + let dt = PyDate::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 as u32, - None, )?; return 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 as u32, + Some( + Py::new(py, FixedTimezone::new(offset, None))? + .to_object(py) + .extract(py)?, + ), + )?; + + return 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 as u32, + None, + )?; + + return Ok(dt.to_object(py)); + } + }, + (_, _) => Err(exceptions::PyValueError::new_err(format!("Parsing error"))), }, (None, Some(duration), None) => { return Ok(Py::new( diff --git a/rust/python/types/timezone.rs b/rust/python/types/timezone.rs index c0688fef..c3a5565a 100644 --- a/rust/python/types/timezone.rs +++ b/rust/python/types/timezone.rs @@ -1,7 +1,8 @@ use pyo3::prelude::*; -use pyo3::types::{PyDateTime, PyDelta, PyTzInfo}; +use pyo3::types::{PyDateTime, PyDelta, PyDict, PyTzInfo}; #[pyclass(module = "_pendulum", extends = PyTzInfo)] +#[derive(Clone)] pub struct FixedTimezone { offset: i32, name: Option, @@ -14,15 +15,15 @@ impl FixedTimezone { Self { offset, name } } - fn utcoffset<'p>(&self, py: Python<'p>, _dt: &PyDateTime) -> PyResult<&'p PyDelta> { + fn utcoffset<'p>(&self, py: Python<'p>, _dt: &PyAny) -> PyResult<&'p PyDelta> { PyDelta::new(py, 0, self.offset, 0, true) } - fn tzname(&self, _dt: &PyDateTime) -> String { + fn tzname(&self, _dt: &PyAny) -> String { self.__str__() } - fn dst<'p>(&self, py: Python<'p>, _dt: &PyDateTime) -> PyResult<&'p PyDelta> { + fn dst<'p>(&self, py: Python<'p>, _dt: &PyAny) -> PyResult<&'p PyDelta> { PyDelta::new(py, 0, 0, 0, true) } @@ -45,4 +46,8 @@ impl FixedTimezone { } } } + + fn __deepcopy__(&self, py: Python, _memo: &PyDict) -> PyResult> { + Py::new(py, self.clone()) + } } diff --git a/tests/parsing/test_parse_iso8601.py b/tests/parsing/test_parse_iso8601.py index 455234ea..22c43a22 100644 --- a/tests/parsing/test_parse_iso8601.py +++ b/tests/parsing/test_parse_iso8601.py @@ -2,6 +2,7 @@ from datetime import date from datetime import datetime +from datetime import time import pytest @@ -17,17 +18,29 @@ @pytest.mark.parametrize( ["text", "expected"], [ - ("2016-10", datetime(2016, 10, 1)), - ("2016-10-06", datetime(2016, 10, 6)), + ("2016-10", date(2016, 10, 1)), + ("2016-10-06", date(2016, 10, 6)), # Ordinal date - ("2012-007", datetime(2012, 1, 7)), - ("2012007", datetime(2012, 1, 7)), - ("2017-079", datetime(2017, 3, 20)), + ("2012-007", date(2012, 1, 7)), + ("2012007", date(2012, 1, 7)), + ("2017-079", date(2017, 3, 20)), # Week date - ("2012-W05", datetime(2012, 1, 30)), - ("2008-W39-6", datetime(2008, 9, 27)), - ("2009-W53-7", datetime(2010, 1, 3)), - ("2009-W01-1", datetime(2008, 12, 29)), + ("2012-W05", date(2012, 1, 30)), + ("2008-W39-6", date(2008, 9, 27)), + ("2009-W53-7", date(2010, 1, 3)), + ("2009-W01-1", date(2008, 12, 29)), + # Time + ("12:34", time(12, 34, 0)), + ("12:34:56", time(12, 34, 56)), + ("12:34:56.123", time(12, 34, 56, 123000)), + ("12:34:56.123456", time(12, 34, 56, 123456)), + ("12:34+05:30", time(12, 34, 0, tzinfo=FixedTimezone(19800))), + ("12:34:56+05:30", time(12, 34, 56, tzinfo=FixedTimezone(19800))), + ("12:34:56.123+05:30", time(12, 34, 56, 123000, tzinfo=FixedTimezone(19800))), + ( + "12:34:56.123456+05:30", + time(12, 34, 56, 123456, tzinfo=FixedTimezone(19800)), + ), # Datetime ("2016-10-06T12:34:56.123456", datetime(2016, 10, 6, 12, 34, 56, 123456)), ("2016-10-06T12:34:56.123", datetime(2016, 10, 6, 12, 34, 56, 123000)), From 07a037e827e0955b6d6569b5d86fdfc462422197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Tue, 18 Jul 2023 21:40:42 +0200 Subject: [PATCH 06/19] Fix parsing of previously supported formats --- rust/parsing.rs | 45 +++++++++++++++++++++++++++++++++++ rust/python/parsing.rs | 8 ++++--- tests/parsing/test_parsing.py | 15 +----------- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/rust/parsing.rs b/rust/parsing.rs index 2af5b976..65f850c9 100644 --- a/rust/parsing.rs +++ b/rust/parsing.rs @@ -33,6 +33,7 @@ pub struct ParsedDateTime { 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, @@ -51,6 +52,7 @@ impl<'a> ParsedDateTime { microsecond: 0, offset: None, has_offset: false, + tzname: None, has_date: false, has_time: false, extended_date_format: false, @@ -432,6 +434,8 @@ impl<'a> Parser<'a> { 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!( @@ -601,6 +605,8 @@ impl<'a> Parser<'a> { } else if !self.end() { tzminute = self.parse_integer(2, "timezone minute")? as i32; } + } else { + datetime.tzname = Some("UTC".to_string()) } if tzminute > 59 { @@ -652,6 +658,15 @@ impl<'a> Parser<'a> { if got_t { match self.current { 'H' => { + if duration.minutes != 0 + || duration.seconds != 0 + || duration.microseconds != 0 + { + return Err( + self.parse_error(format!("Duration units out of order")) + ); + } + duration.hours += value; if let Some(fraction) = op_fraction { @@ -670,6 +685,12 @@ impl<'a> Parser<'a> { } } 'M' => { + if duration.seconds != 0 || duration.microseconds != 0 { + return Err( + self.parse_error(format!("Duration units out of order")) + ); + } + duration.minutes += value; if let Some(fraction) = op_fraction { @@ -704,6 +725,12 @@ impl<'a> Parser<'a> { ))); } + if duration.months != 0 || duration.days != 0 { + return Err( + self.parse_error(format!("Duration units out of order")) + ); + } + duration.years = value; } 'M' => { @@ -713,9 +740,21 @@ impl<'a> Parser<'a> { ))); } + if duration.days != 0 { + return Err( + self.parse_error(format!("Duration units out of order")) + ); + } + duration.months = value; } 'W' => { + if duration.years != 0 || duration.months != 0 { + return Err(self.parse_error(format!( + "Basic format durations cannot have weeks" + ))); + } + duration.weeks = value; if let Some(fraction) = op_fraction { @@ -741,6 +780,12 @@ impl<'a> Parser<'a> { } } 'D' => { + if duration.weeks != 0 { + return Err(self.parse_error(format!( + "Week format durations cannot have days" + ))); + } + duration.days += value; if let Some(fraction) = op_fraction { let extra_hours = fraction * 24.0; diff --git a/rust/python/parsing.rs b/rust/python/parsing.rs index 16113e86..70fb1501 100644 --- a/rust/python/parsing.rs +++ b/rust/python/parsing.rs @@ -26,7 +26,7 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { datetime.second as u8, datetime.microsecond as u32, Some( - Py::new(py, FixedTimezone::new(offset, None))? + Py::new(py, FixedTimezone::new(offset, datetime.tzname))? .to_object(py) .extract(py)?, ), @@ -69,7 +69,7 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { datetime.second as u8, datetime.microsecond as u32, Some( - Py::new(py, FixedTimezone::new(offset, None))? + Py::new(py, FixedTimezone::new(offset, datetime.tzname))? .to_object(py) .extract(py)?, ), @@ -108,7 +108,9 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { )? .to_object(py)); } - (_, _, _) => todo!(), + (_, _, _) => Err(exceptions::PyValueError::new_err(format!( + "Not yet implemented" + ))), }, Err(error) => Err(exceptions::PyValueError::new_err(format!("{}", error))), } diff --git a/tests/parsing/test_parsing.py b/tests/parsing/test_parsing.py index 35dcf868..d57b82f8 100644 --- a/tests/parsing/test_parsing.py +++ b/tests/parsing/test_parsing.py @@ -276,19 +276,6 @@ def test_iso8601_datetime(): assert parsed.microsecond == 0 assert parsed.utcoffset().total_seconds() == 19800 - text = "20161001T1430,4+0530" - - parsed = parse(text) - - assert parsed.year == 2016 - assert parsed.month == 10 - assert parsed.day == 1 - assert parsed.hour == 14 - assert parsed.minute == 30 - assert parsed.second == 0 - assert parsed.microsecond == 400000 - assert parsed.utcoffset().total_seconds() == 19800 - text = "2008-09-03T20:56:35.450686+01" parsed = parse(text) @@ -480,7 +467,7 @@ def test_iso8601_ordinal(): def test_iso8601_time(): now = pendulum.datetime(2015, 11, 12) - text = "201205" + text = "T201205" parsed = parse(text, now=now) From ef3e0beaa2d679020a54bad1bf51ea69ca59a95a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Tue, 18 Jul 2023 21:53:25 +0200 Subject: [PATCH 07/19] No longer build the _iso8601 extension --- meson.build | 1 - pendulum/parsing/_iso8601.pyi | 21 --------------------- pyproject.toml | 2 -- 3 files changed, 24 deletions(-) delete mode 100644 pendulum/parsing/_iso8601.pyi diff --git a/meson.build b/meson.build index 666c2810..fbbfb9e2 100644 --- a/meson.build +++ b/meson.build @@ -6,7 +6,6 @@ py_dep = py.dependency() extensions = [ ['_helpers', 'pendulum/_extensions/_helpers.c', meson.source_root() / 'pendulum/_extensions/'], - ['_iso8601', 'pendulum/parsing/_iso8601.c', meson.source_root() / 'pendulum/parsing/'], ] foreach extension : extensions diff --git a/pendulum/parsing/_iso8601.pyi b/pendulum/parsing/_iso8601.pyi deleted file mode 100644 index 761fe6b3..00000000 --- a/pendulum/parsing/_iso8601.pyi +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import annotations - -from datetime import date -from datetime import datetime -from datetime import time - -class Duration: - years: int = 0 - months: int = 0 - weeks: int = 0 - days: int = 0 - remaining_days: int = 0 - hours: int = 0 - minutes: int = 0 - seconds: int = 0 - remaining_seconds: int = 0 - microseconds: int = 0 - -def parse_iso8601( - text: str, -) -> datetime | date | time | Duration: ... diff --git a/pyproject.toml b/pyproject.toml index 35e63f99..20cf653f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,6 @@ include = [ # C extensions must be included in the wheel distributions { path = "pendulum/_extensions/*.so", format = "wheel" }, { path = "pendulum/_extensions/*.pyd", format = "wheel" }, - { path = "pendulum/parsing/*.so", format = "wheel" }, - { path = "pendulum/parsing/*.pyd", format = "wheel" }, # Rust extension { path = "_pendulum*.so", format = "wheel" }, { path = "_pendulum*.pyd", format = "wheel" }, From 00ba22d8ec9552c9bfc8816f01693fe8baf953ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Tue, 18 Jul 2023 22:03:32 +0200 Subject: [PATCH 08/19] Fix typing --- _pendulum.pyi | 21 +++++++++++++++++++++ pendulum/parser.py | 2 +- pyproject.toml | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 _pendulum.pyi diff --git a/_pendulum.pyi b/_pendulum.pyi new file mode 100644 index 00000000..761fe6b3 --- /dev/null +++ b/_pendulum.pyi @@ -0,0 +1,21 @@ +from __future__ import annotations + +from datetime import date +from datetime import datetime +from datetime import time + +class Duration: + years: int = 0 + months: int = 0 + weeks: int = 0 + days: int = 0 + remaining_days: int = 0 + hours: int = 0 + minutes: int = 0 + seconds: int = 0 + remaining_seconds: int = 0 + microseconds: int = 0 + +def parse_iso8601( + text: str, +) -> datetime | date | time | Duration: ... diff --git a/pendulum/parser.py b/pendulum/parser.py index c364789b..77f43752 100644 --- a/pendulum/parser.py +++ b/pendulum/parser.py @@ -20,7 +20,7 @@ try: from _pendulum import Duration as CDuration except ImportError: - CDuration = None + CDuration = None # type: ignore[assignment,misc] def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration: diff --git a/pyproject.toml b/pyproject.toml index 20cf653f..b13daf00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ keywords = ['datetime', 'date', 'time'] packages = [ { include = "pendulum" }, { include = "tests", format = "sdist" }, + { include = "*.pyi"}, ] include = [ { path = "meson.build", format = "sdist" }, From a90c1a7a534901ebc6e1d15343670b42afbd57b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Tue, 18 Jul 2023 22:05:49 +0200 Subject: [PATCH 09/19] Include the rust directory in the sdist --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b13daf00..87359547 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ keywords = ['datetime', 'date', 'time'] packages = [ { include = "pendulum" }, + { include = "rust", format = "sdist" }, { include = "tests", format = "sdist" }, { include = "*.pyi"}, ] From 008d5fa7b6ce093ec4fbe1a2b64300bcd535097d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Tue, 18 Jul 2023 22:09:43 +0200 Subject: [PATCH 10/19] Fix includes in the pyproject file --- pyproject.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 87359547..35f9269e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,9 +12,7 @@ keywords = ['datetime', 'date', 'time'] packages = [ { include = "pendulum" }, - { include = "rust", format = "sdist" }, { include = "tests", format = "sdist" }, - { include = "*.pyi"}, ] include = [ { path = "meson.build", format = "sdist" }, @@ -22,6 +20,10 @@ include = [ # C extensions must be included in the wheel distributions { path = "pendulum/_extensions/*.so", format = "wheel" }, { path = "pendulum/_extensions/*.pyd", format = "wheel" }, + # Typing stubs + { path = "*.pyi"}, + # Rust source + { path = "rust", format = "sdist" }, # Rust extension { path = "_pendulum*.so", format = "wheel" }, { path = "_pendulum*.pyd", format = "wheel" }, From cb5d28d186c11312539db34cc4a1e46c9c924036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Tue, 18 Jul 2023 22:23:10 +0200 Subject: [PATCH 11/19] Ensure extensions are compiled in CI --- .github/workflows/codspeed.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 3d601cea..01bfd52a 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -39,7 +39,12 @@ jobs: run: poetry config virtualenvs.create false - name: Install dependencies - run: poetry install --only main --only test --only benchmark -vvv + run: poetry install --only main --only test --only benchmark -vvv --no-root + + - name: Install pendulum and check extensions + run: | + poetry install --only-root + python -c 'import _pendulum; from pendulum._extensions import _helpers' - name: Run benchmarks uses: CodSpeedHQ/action@v1 From a5d531a6d06c58cefccb41adecfa46fd184e48ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Tue, 18 Jul 2023 23:51:19 +0200 Subject: [PATCH 12/19] Optimize build --- .cargo/config.toml | 15 +++++++ .github/workflows/codspeed.yml | 12 ++---- Cargo.lock | 79 ++++++++++++++++++---------------- Cargo.toml | 14 +++--- build.py | 7 ++- pyproject.toml | 2 +- rust/lib.rs | 4 ++ rust/python/mod.rs | 3 ++ 8 files changed, 83 insertions(+), 53 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..f0ba8af4 --- /dev/null +++ b/.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/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 01bfd52a..71604470 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -25,26 +25,22 @@ jobs: - name: Install poetry run: | - curl -fsS https://install.python-poetry.org | python - --preview -y + curl -fsS https://install.python-poetry.org | python - -y - name: Update PATH if: ${{ matrix.os != 'Windows' }} run: echo "$HOME/.local/bin" >> $GITHUB_PATH - - name: Update Path for Windows - if: ${{ matrix.os == 'Windows' }} - run: echo "$APPDATA\Python\Scripts" >> $GITHUB_PATH - - name: Configure poetry run: poetry config virtualenvs.create false - name: Install dependencies - run: poetry install --only main --only test --only benchmark -vvv --no-root + run: poetry install --only test --only benchmark -vvv --no-root - name: Install pendulum and check extensions run: | - poetry install --only-root - python -c 'import _pendulum; from pendulum._extensions import _helpers' + MATURIN_BUILD_ARGS="--no-default-features -vv" pip install -e . -v + python -c 'import _pendulum; from pendulum._extensions import _helpers; assert _pendulum.__pendulum_default_allocator__' - name: Run benchmarks uses: CodSpeedHQ/action@v1 diff --git a/Cargo.lock b/Cargo.lock index 42aa1cc2..4d0fd72b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,12 @@ 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" @@ -26,21 +32,22 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" -[[package]] -name = "iso8601" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" -dependencies = [ - "nom", -] - [[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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ac0e912c8ef1b735e92369695618dc5b1819f5a7bf3f167301a3ba1cea515e" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "lock_api" version = "0.4.9" @@ -51,12 +58,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - [[package]] name = "memoffset" version = "0.9.0" @@ -67,19 +68,12 @@ dependencies = [ ] [[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "nom" -version = "7.1.3" +name = "mimalloc" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "4e2894987a3459f3ffb755608bd82188f8ed00d0ae077f1edea29c068d639d98" dependencies = [ - "memchr", - "minimal-lexical", + "libmimalloc-sys", ] [[package]] @@ -115,8 +109,7 @@ dependencies = [ name = "pendulum" version = "3.0.0-alpha-1" dependencies = [ - "iso8601", - "nom", + "mimalloc", "pyo3", ] @@ -131,9 +124,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffef52f74ec3b1a1baf295d9b8fcc3070327aefc39a6d00656b13c1d0b8885c" +checksum = "ffb88ae05f306b4bfcde40ac4a51dc0b05936a9207a4b75b798c7729c4258a59" dependencies = [ "cfg-if", "indoc", @@ -148,19 +141,20 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713eccf888fb05f1a96eb78c0dbc51907fee42b3377272dc902eb38985f418d5" +checksum = "554db24f0b3c180a9c0b1268f91287ab3f17c162e15b54caaae5a6b3773396b0" dependencies = [ "once_cell", + "python3-dll-a", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b2ecbdcfb01cbbf56e179ce969a048fd7305a66d4cdf3303e0da09d69afe4c3" +checksum = "922ede8759e8600ad4da3195ae41259654b9c55da4f7eec84a0ccc7d067a70a4" dependencies = [ "libc", "pyo3-build-config", @@ -168,9 +162,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b78fdc0899f2ea781c463679b20cb08af9247febc8d052de941951024cd8aea0" +checksum = "8a5caec6a1dd355964a841fcbeeb1b89fe4146c87295573f94228911af3cc5a2" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -180,15 +174,24 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60da7b84f1227c3e2fe7593505de274dcf4c8928b4e0a1c23d551a14e4e80a0f" +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" diff --git a/Cargo.toml b/Cargo.toml index 737ec459..0c3176a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,14 +5,18 @@ edition = "2021" [lib] name = "_pendulum" -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] path = "rust/lib.rs" +[profile.release] +lto = "fat" +codegen-units = 1 +strip = true + [dependencies] -iso8601 = "0.6.1" -nom = "7.1.3" -pyo3 = { version = "0.19.0", features = ["extension-module"] } +pyo3 = { version = "0.19.0", features = ["extension-module", "generate-import-lib"] } +mimalloc = { version = "0.1.30", optional = true, default-features = false } [features] extension-module = ["pyo3/extension-module"] -default = ["extension-module"] +default = ["mimalloc"] diff --git a/build.py b/build.py index 8fb56882..53b18547 100644 --- a/build.py +++ b/build.py @@ -1,3 +1,5 @@ +import os +import shlex import shutil import subprocess import zipfile @@ -25,7 +27,10 @@ def _build(): if wheels_dir.exists(): shutil.rmtree(wheels_dir) - maturin("build", "-r") + if os.getenv("MATURIN_BUILD_ARGS"): + cargo_args = shlex.split(os.getenv("MATURIN_BUILD_ARGS", "")) + + maturin("build", "-r", *cargo_args) wheel = list(wheels_dir.glob("*.whl"))[0] with zipfile.ZipFile(wheel.as_posix()) as whl: diff --git a/pyproject.toml b/pyproject.toml index 35f9269e..1b30fc07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,5 +224,5 @@ omit = [ ] [build-system] -requires = ["poetry-core>=1.1.0a6", "meson", "ninja", "maturin>=0.14,<0.15"] +requires = ["poetry-core>=1.1.0a6", "meson", "ninja", "maturin>=1,<2"] build-backend = "poetry.core.masonry.api" diff --git a/rust/lib.rs b/rust/lib.rs index aac55da7..bd0f1f6f 100644 --- a/rust/lib.rs +++ b/rust/lib.rs @@ -1,5 +1,9 @@ extern crate core; +#[cfg(feature = "mimalloc")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + mod constants; mod helpers; mod parsing; diff --git a/rust/python/mod.rs b/rust/python/mod.rs index e1b34b45..d0bf657f 100644 --- a/rust/python/mod.rs +++ b/rust/python/mod.rs @@ -17,5 +17,8 @@ pub fn _pendulum(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(parse_iso8601, m)?)?; m.add_class::()?; + #[cfg(not(feature = "mimalloc"))] + m.setattr("__pendulum_default_allocator__", true)?; // uses setattr so this is not in __all__ + Ok(()) } From f66ac8307bb359ff8bd32031f6a89e78b9a34ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Thu, 3 Aug 2023 00:05:02 +0200 Subject: [PATCH 13/19] Implement the helpers extension in Rust --- .github/workflows/codspeed.yml | 2 +- _pendulum.pyi | 20 + build.py | 11 +- meson.build | 19 - pendulum/_extensions/__init__.py | 0 pendulum/_extensions/_helpers.c | 931 ----------- pendulum/_extensions/_helpers.pyi | 26 - .../{_extensions/helpers.py => _helpers.py} | 0 pendulum/helpers.py | 31 +- pendulum/parsing/_iso8601.c | 1361 ----------------- pyproject.toml | 2 +- rust/helpers.rs | 27 +- rust/parsing.rs | 22 +- rust/python/helpers.rs | 367 ++++- rust/python/mod.rs | 7 +- rust/python/types/mod.rs | 2 + rust/python/types/precise_diff.rs | 52 + tests/test_helpers.py | 5 + 18 files changed, 490 insertions(+), 2395 deletions(-) delete mode 100644 meson.build delete mode 100644 pendulum/_extensions/__init__.py delete mode 100644 pendulum/_extensions/_helpers.c delete mode 100644 pendulum/_extensions/_helpers.pyi rename pendulum/{_extensions/helpers.py => _helpers.py} (100%) delete mode 100644 pendulum/parsing/_iso8601.c create mode 100644 rust/python/types/precise_diff.rs diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 71604470..df1754b8 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -40,7 +40,7 @@ jobs: - name: Install pendulum and check extensions run: | MATURIN_BUILD_ARGS="--no-default-features -vv" pip install -e . -v - python -c 'import _pendulum; from pendulum._extensions import _helpers; assert _pendulum.__pendulum_default_allocator__' + python -c 'import _pendulum; assert _pendulum.__pendulum_default_allocator__' - name: Run benchmarks uses: CodSpeedHQ/action@v1 diff --git a/_pendulum.pyi b/_pendulum.pyi index 761fe6b3..02520a14 100644 --- a/_pendulum.pyi +++ b/_pendulum.pyi @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import date from datetime import datetime from datetime import time +from typing import NamedTuple class Duration: years: int = 0 @@ -16,6 +17,25 @@ class Duration: remaining_seconds: int = 0 microseconds: int = 0 +class PreciseDiff(NamedTuple): + years: int + months: int + days: int + hours: int + minutes: int + seconds: int + microseconds: int + total_days: int + def parse_iso8601( text: str, ) -> datetime | date | time | Duration: ... +def days_in_year(year: int) -> int: ... +def is_leap(year: int) -> bool: ... +def is_long_year(year: int) -> bool: ... +def local_time( + unix_time: int, utc_offset: int, microseconds: int +) -> tuple[int, int, int, int, int, int, int]: ... +def precise_diff(d1: datetime | date, d2: datetime | date) -> PreciseDiff: ... +def timestamp(dt: datetime) -> int: ... +def week_day(year: int, month: int, day: int) -> int: ... diff --git a/build.py b/build.py index 53b18547..4ff12214 100644 --- a/build.py +++ b/build.py @@ -7,26 +7,19 @@ from pathlib import Path -def meson(*args): - subprocess.call(["meson", *list(args)]) - - def maturin(*args): - subprocess.call(["maturin"] + list(args)) + subprocess.call(["maturin", *list(args)]) def _build(): build_dir = Path(__file__).parent.joinpath("build") build_dir.mkdir(parents=True, exist_ok=True) - meson("setup", build_dir.as_posix()) - meson("compile", "-C", build_dir.as_posix()) - meson("install", "-C", build_dir.as_posix()) - wheels_dir = Path(__file__).parent.joinpath("target/wheels") if wheels_dir.exists(): shutil.rmtree(wheels_dir) + cargo_args = [] if os.getenv("MATURIN_BUILD_ARGS"): cargo_args = shlex.split(os.getenv("MATURIN_BUILD_ARGS", "")) diff --git a/meson.build b/meson.build deleted file mode 100644 index fbbfb9e2..00000000 --- a/meson.build +++ /dev/null @@ -1,19 +0,0 @@ -project('pendulum C extensions', 'c') - -py_mod = import('python') -py = py_mod.find_installation() -py_dep = py.dependency() - -extensions = [ - ['_helpers', 'pendulum/_extensions/_helpers.c', meson.source_root() / 'pendulum/_extensions/'], -] - -foreach extension : extensions - py.extension_module( - extension[0], - extension[1], - dependencies : py_dep, - install : true, - install_dir: extension[2] - ) -endforeach diff --git a/pendulum/_extensions/__init__.py b/pendulum/_extensions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pendulum/_extensions/_helpers.c b/pendulum/_extensions/_helpers.c deleted file mode 100644 index a3114d9d..00000000 --- a/pendulum/_extensions/_helpers.c +++ /dev/null @@ -1,931 +0,0 @@ -/* ------------------------------------------------------------------------- */ - -#include -#include -#include -#include -#include -#include -#include -#include - -/* ------------------------------------------------------------------------- */ - -#define EPOCH_YEAR 1970 - -#define DAYS_PER_N_YEAR 365 -#define DAYS_PER_L_YEAR 366 - -#define USECS_PER_SEC 1000000 - -#define SECS_PER_MIN 60 -#define SECS_PER_HOUR (60 * SECS_PER_MIN) -#define SECS_PER_DAY (SECS_PER_HOUR * 24) - -// 400-year chunks always have 146097 days (20871 weeks). -#define DAYS_PER_400_YEARS 146097L -#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY) - -// 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. -const int64_t SECS_PER_100_YEARS[2] = { - (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY}; - -// 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. -const int32_t SECS_PER_4_YEARS[2] = { - (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY}; - -// The number of seconds in non-leap and leap years respectively. -const int32_t SECS_PER_YEAR[2] = { - DAYS_PER_N_YEAR * SECS_PER_DAY, - DAYS_PER_L_YEAR *SECS_PER_DAY}; - -#define MONTHS_PER_YEAR 12 - -// The month lengths in non-leap and leap years respectively. -const int32_t DAYS_PER_MONTHS[2][13] = { - {-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. -const int32_t MONTHS_OFFSETS[2][14] = { - {-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}}; - -const int DAY_OF_WEEK_TABLE[12] = { - 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; - -#define TM_SUNDAY 0 -#define TM_MONDAY 1 -#define TM_TUESDAY 2 -#define TM_WEDNESDAY 3 -#define TM_THURSDAY 4 -#define TM_FRIDAY 5 -#define TM_SATURDAY 6 - -#define TM_JANUARY 0 -#define TM_FEBRUARY 1 -#define TM_MARCH 2 -#define TM_APRIL 3 -#define TM_MAY 4 -#define TM_JUNE 5 -#define TM_JULY 6 -#define TM_AUGUST 7 -#define TM_SEPTEMBER 8 -#define TM_OCTOBER 9 -#define TM_NOVEMBER 10 -#define TM_DECEMBER 11 - -/* ------------------------------------------------------------------------- */ - -int _p(int y) -{ - return y + y / 4 - y / 100 + y / 400; -} - -int _is_leap(int year) -{ - return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); -} - -int _is_long_year(int year) -{ - return (_p(year) % 7 == 4) || (_p(year - 1) % 7 == 3); -} - -int _week_day(int year, int month, int day) -{ - int y; - int w; - - y = year - (month < 3); - - w = (_p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7; - - if (!w) - { - w = 7; - } - - return w; -} - -int _days_in_year(int year) -{ - if (_is_leap(year)) - { - return DAYS_PER_L_YEAR; - } - - return DAYS_PER_N_YEAR; -} - -int _day_number(int year, int month, int day) -{ - month = (month + 9) % 12; - year = year - month / 10; - - return ( - 365 * year + year / 4 - year / 100 + year / 400 + (month * 306 + 5) / 10 + (day - 1)); -} - -int _get_offset(PyObject *dt) -{ - PyObject *tzinfo; - PyObject *offset; - - tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo; - - if (tzinfo != Py_None) - { - offset = PyObject_CallMethod(tzinfo, "utcoffset", "O", dt); - - return PyDateTime_DELTA_GET_DAYS(offset) * SECS_PER_DAY + PyDateTime_DELTA_GET_SECONDS(offset); - } - - return 0; -} - -int _has_tzinfo(PyObject *dt) -{ - return ((_PyDateTime_BaseTZInfo *)(dt))->hastzinfo; -} - -char *_get_tz_name(PyObject *dt) -{ - PyObject *tzinfo; - char *tz = ""; - - tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo; - - if (tzinfo != Py_None) - { - if (PyObject_HasAttrString(tzinfo, "key")) - { - // zoneinfo timezone - tz = (char *)PyUnicode_AsUTF8( - PyObject_GetAttrString(tzinfo, "name")); - } - else if (PyObject_HasAttrString(tzinfo, "name")) - { - // Pendulum timezone - tz = (char *)PyUnicode_AsUTF8( - PyObject_GetAttrString(tzinfo, "name")); - } - else if (PyObject_HasAttrString(tzinfo, "zone")) - { - // pytz timezone - tz = (char *)PyUnicode_AsUTF8( - PyObject_GetAttrString(tzinfo, "zone")); - } - } - - return tz; -} - -/* ------------------------ Custom Types ------------------------------- */ - -/* - * class Diff(): - */ -typedef struct -{ - PyObject_HEAD int years; - int months; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - int total_days; -} Diff; - -/* - * def __init__(self, years, months, days, hours, minutes, seconds, microseconds, total_days): - * self.years = years - * # ... -*/ -static int Diff_init(Diff *self, PyObject *args, PyObject *kwargs) -{ - int years; - int months; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - int total_days; - - if (!PyArg_ParseTuple(args, "iiiiiii", &years, &months, &days, &hours, &minutes, &seconds, µseconds, &total_days)) - return -1; - - self->years = years; - self->months = months; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - self->total_days = total_days; - - return 0; -} - -/* - * def __repr__(self): - * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format( - * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds - * ) - */ -static PyObject *Diff_repr(Diff *self) -{ - return PyUnicode_FromFormat( - "%d years %d months %d days %d hours %d minutes %d seconds %d microseconds", - self->years, - self->months, - self->days, - self->hours, - self->minutes, - self->seconds, - self->microseconds); -} - -/* - * Instantiate new Diff_type object - * Skip overhead of calling PyObject_New and PyObject_Init. - * Directly allocate object. - */ -static PyObject *new_diff_ex(int years, int months, int days, int hours, int minutes, int seconds, int microseconds, int total_days, PyTypeObject *type) -{ - Diff *self = (Diff *)(type->tp_alloc(type, 0)); - - if (self != NULL) - { - self->years = years; - self->months = months; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - self->total_days = total_days; - } - - return (PyObject *)self; -} - -/* - * Class member / class attributes - */ -static PyMemberDef Diff_members[] = { - {"years", T_INT, offsetof(Diff, years), 0, "years in diff"}, - {"months", T_INT, offsetof(Diff, months), 0, "months in diff"}, - {"days", T_INT, offsetof(Diff, days), 0, "days in diff"}, - {"hours", T_INT, offsetof(Diff, hours), 0, "hours in diff"}, - {"minutes", T_INT, offsetof(Diff, minutes), 0, "minutes in diff"}, - {"seconds", T_INT, offsetof(Diff, seconds), 0, "seconds in diff"}, - {"microseconds", T_INT, offsetof(Diff, microseconds), 0, "microseconds in diff"}, - {"total_days", T_INT, offsetof(Diff, total_days), 0, "total days in diff"}, - {NULL}}; - -static PyTypeObject Diff_type = { - PyVarObject_HEAD_INIT(NULL, 0) "PreciseDiff", /* tp_name */ - sizeof(Diff), /* tp_basicsize */ - 0, /* tp_itemsize */ - 0, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_as_async */ - (reprfunc)Diff_repr, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - (reprfunc)Diff_repr, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - "Precise difference between two datetime objects", /* tp_doc */ -}; - -#define new_diff(years, months, days, hours, minutes, seconds, microseconds, total_days) new_diff_ex(years, months, days, hours, minutes, seconds, microseconds, total_days, &Diff_type) - -/* -------------------------- Functions --------------------------*/ - -PyObject *is_leap(PyObject *self, PyObject *args) -{ - PyObject *leap; - int year; - - if (!PyArg_ParseTuple(args, "i", &year)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - leap = PyBool_FromLong(_is_leap(year)); - - return leap; -} - -PyObject *is_long_year(PyObject *self, PyObject *args) -{ - PyObject *is_long; - int year; - - if (!PyArg_ParseTuple(args, "i", &year)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - is_long = PyBool_FromLong(_is_long_year(year)); - - return is_long; -} - -PyObject *week_day(PyObject *self, PyObject *args) -{ - PyObject *wd; - int year; - int month; - int day; - - if (!PyArg_ParseTuple(args, "iii", &year, &month, &day)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - wd = PyLong_FromLong(_week_day(year, month, day)); - - return wd; -} - -PyObject *days_in_year(PyObject *self, PyObject *args) -{ - PyObject *ndays; - int year; - - if (!PyArg_ParseTuple(args, "i", &year)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - ndays = PyLong_FromLong(_days_in_year(year)); - - return ndays; -} - -PyObject *timestamp(PyObject *self, PyObject *args) -{ - int64_t result; - PyObject *dt; - - if (!PyArg_ParseTuple(args, "O", &dt)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - int year = (double)PyDateTime_GET_YEAR(dt); - int month = PyDateTime_GET_MONTH(dt); - int day = PyDateTime_GET_DAY(dt); - int hour = PyDateTime_DATE_GET_HOUR(dt); - int minute = PyDateTime_DATE_GET_MINUTE(dt); - int second = PyDateTime_DATE_GET_SECOND(dt); - - result = (year - 1970) * 365 + MONTHS_OFFSETS[0][month]; - result += (int)floor((double)(year - 1968) / 4); - result -= (year - 1900) / 100; - result += (year - 1600) / 400; - - if (_is_leap(year) && month < 3) - { - result -= 1; - } - - result += day - 1; - result *= 24; - result += hour; - result *= 60; - result += minute; - result *= 60; - result += second; - - return PyLong_FromSsize_t(result); -} - -PyObject *local_time(PyObject *self, PyObject *args) -{ - double unix_time; - int32_t utc_offset; - int32_t year; - int32_t microsecond; - int64_t seconds; - int32_t leap_year; - int64_t sec_per_100years; - int64_t sec_per_4years; - int32_t sec_per_year; - int32_t month; - int32_t day; - int32_t month_offset; - int32_t hour; - int32_t minute; - int32_t second; - - if (!PyArg_ParseTuple(args, "dii", &unix_time, &utc_offset, µsecond)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - year = EPOCH_YEAR; - seconds = (int64_t)floor(unix_time); - - // Shift to a base year that is 400-year aligned. - if (seconds >= 0) - { - seconds -= 10957L * SECS_PER_DAY; - year += 30; // == 2000; - } - else - { - seconds += (int64_t)(146097L - 10957L) * SECS_PER_DAY; - year -= 370; // == 1600; - } - - seconds += utc_offset; - - // Handle years in chunks of 400/100/4/1 - year += 400 * (seconds / SECS_PER_400_YEARS); - seconds %= SECS_PER_400_YEARS; - if (seconds < 0) - { - seconds += SECS_PER_400_YEARS; - year -= 400; - } - - leap_year = 1; // 4-century aligned - - sec_per_100years = SECS_PER_100_YEARS[leap_year]; - - 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]; - } - - sec_per_4years = SECS_PER_4_YEARS[leap_year]; - 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]; - } - - sec_per_year = SECS_PER_YEAR[leap_year]; - 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]; - } - - // Handle months and days - month = TM_DECEMBER + 1; - day = seconds / SECS_PER_DAY + 1; - seconds %= SECS_PER_DAY; - while (month != TM_JANUARY + 1) - { - month_offset = MONTHS_OFFSETS[leap_year][month]; - if (day > month_offset) - { - day -= month_offset; - break; - } - - month -= 1; - } - - // Handle hours, minutes and seconds - hour = seconds / SECS_PER_HOUR; - seconds %= SECS_PER_HOUR; - minute = seconds / SECS_PER_MIN; - second = seconds % SECS_PER_MIN; - - return Py_BuildValue("NNNNNNN", - PyLong_FromLong(year), - PyLong_FromLong(month), - PyLong_FromLong(day), - PyLong_FromLong(hour), - PyLong_FromLong(minute), - PyLong_FromLong(second), - PyLong_FromLong(microsecond)); -} - -// Calculate a precise difference between two datetimes. -PyObject *precise_diff(PyObject *self, PyObject *args) -{ - PyObject *dt1; - PyObject *dt2; - - if (!PyArg_ParseTuple(args, "OO", &dt1, &dt2)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - int year_diff = 0; - int month_diff = 0; - int day_diff = 0; - int hour_diff = 0; - int minute_diff = 0; - int second_diff = 0; - int microsecond_diff = 0; - int sign = 1; - int year; - int month; - int leap; - int days_in_last_month; - int days_in_month; - int dt1_year = PyDateTime_GET_YEAR(dt1); - int dt2_year = PyDateTime_GET_YEAR(dt2); - int dt1_month = PyDateTime_GET_MONTH(dt1); - int dt2_month = PyDateTime_GET_MONTH(dt2); - int dt1_day = PyDateTime_GET_DAY(dt1); - int dt2_day = PyDateTime_GET_DAY(dt2); - int dt1_hour = 0; - int dt2_hour = 0; - int dt1_minute = 0; - int dt2_minute = 0; - int dt1_second = 0; - int dt2_second = 0; - int dt1_microsecond = 0; - int dt2_microsecond = 0; - int dt1_total_seconds = 0; - int dt2_total_seconds = 0; - int dt1_offset = 0; - int dt2_offset = 0; - int dt1_is_datetime = PyDateTime_Check(dt1); - int dt2_is_datetime = PyDateTime_Check(dt2); - char *tz1 = ""; - char *tz2 = ""; - int in_same_tz = 0; - int total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day)); - - // If both dates are datetimes, we check - // If we are in the same timezone - if (dt1_is_datetime && dt2_is_datetime) - { - if (_has_tzinfo(dt1)) - { - tz1 = _get_tz_name(dt1); - dt1_offset = _get_offset(dt1); - } - - if (_has_tzinfo(dt2)) - { - tz2 = _get_tz_name(dt2); - dt2_offset = _get_offset(dt2); - } - - in_same_tz = tz1 == tz2 && strncmp(tz1, "", 1); - } - - // If we have datetimes (and not only dates) - // we get the information we need - if (dt1_is_datetime) - { - dt1_hour = PyDateTime_DATE_GET_HOUR(dt1); - dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1); - dt1_second = PyDateTime_DATE_GET_SECOND(dt1); - dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1); - - if ((!in_same_tz && dt1_offset != 0) || total_days == 0) - { - dt1_hour -= dt1_offset / SECS_PER_HOUR; - dt1_offset %= SECS_PER_HOUR; - dt1_minute -= dt1_offset / SECS_PER_MIN; - dt1_offset %= SECS_PER_MIN; - dt1_second -= dt1_offset; - - if (dt1_second < 0) - { - dt1_second += 60; - dt1_minute -= 1; - } - else if (dt1_second > 60) - { - dt1_second -= 60; - dt1_minute += 1; - } - - if (dt1_minute < 0) - { - dt1_minute += 60; - dt1_hour -= 1; - } - else if (dt1_minute > 60) - { - dt1_minute -= 60; - dt1_hour += 1; - } - - if (dt1_hour < 0) - { - dt1_hour += 24; - dt1_day -= 1; - } - else if (dt1_hour > 24) - { - dt1_hour -= 24; - dt1_day += 1; - } - } - - dt1_total_seconds = (dt1_hour * SECS_PER_HOUR + dt1_minute * SECS_PER_MIN + dt1_second); - } - - if (dt2_is_datetime) - { - dt2_hour = PyDateTime_DATE_GET_HOUR(dt2); - dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2); - dt2_second = PyDateTime_DATE_GET_SECOND(dt2); - dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2); - - if ((!in_same_tz && dt2_offset != 0) || total_days == 0) - { - dt2_hour -= dt2_offset / SECS_PER_HOUR; - dt2_offset %= SECS_PER_HOUR; - dt2_minute -= dt2_offset / SECS_PER_MIN; - dt2_offset %= SECS_PER_MIN; - dt2_second -= dt2_offset; - - if (dt2_second < 0) - { - dt2_second += 60; - dt2_minute -= 1; - } - else if (dt2_second > 60) - { - dt2_second -= 60; - dt2_minute += 1; - } - - if (dt2_minute < 0) - { - dt2_minute += 60; - dt2_hour -= 1; - } - else if (dt2_minute > 60) - { - dt2_minute -= 60; - dt2_hour += 1; - } - - if (dt2_hour < 0) - { - dt2_hour += 24; - dt2_day -= 1; - } - else if (dt2_hour > 24) - { - dt2_hour -= 24; - dt2_day += 1; - } - } - - dt2_total_seconds = (dt2_hour * SECS_PER_HOUR + dt2_minute * SECS_PER_MIN + dt2_second); - } - - // Direct comparison between two datetimes does not work - // so we need to check by properties - int dt1_gt_dt2 = (dt1_year > dt2_year || (dt1_year == dt2_year && dt1_month > dt2_month) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day > dt2_day) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds > dt2_total_seconds) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds == dt2_total_seconds && dt1_microsecond > dt2_microsecond)); - - if (dt1_gt_dt2) - { - PyObject *temp; - temp = dt1; - dt1 = dt2; - dt2 = temp; - sign = -1; - - // Retrieving properties - dt1_year = PyDateTime_GET_YEAR(dt1); - dt2_year = PyDateTime_GET_YEAR(dt2); - dt1_month = PyDateTime_GET_MONTH(dt1); - dt2_month = PyDateTime_GET_MONTH(dt2); - dt1_day = PyDateTime_GET_DAY(dt1); - dt2_day = PyDateTime_GET_DAY(dt2); - - if (dt2_is_datetime) - { - dt1_hour = PyDateTime_DATE_GET_HOUR(dt1); - dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1); - dt1_second = PyDateTime_DATE_GET_SECOND(dt1); - dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1); - } - - if (dt1_is_datetime) - { - dt2_hour = PyDateTime_DATE_GET_HOUR(dt2); - dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2); - dt2_second = PyDateTime_DATE_GET_SECOND(dt2); - dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2); - } - - total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day)); - } - - year_diff = dt2_year - dt1_year; - month_diff = dt2_month - dt1_month; - day_diff = dt2_day - dt1_day; - hour_diff = dt2_hour - dt1_hour; - minute_diff = dt2_minute - dt1_minute; - second_diff = dt2_second - dt1_second; - microsecond_diff = dt2_microsecond - dt1_microsecond; - - if (microsecond_diff < 0) - { - microsecond_diff += 1e6; - 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 - year = dt2_year; - month = dt2_month; - - if (month == 1) - { - month = 12; - year -= 1; - } - else - { - month -= 1; - } - - leap = _is_leap(year); - - days_in_last_month = DAYS_PER_MONTHS[leap][month]; - days_in_month = DAYS_PER_MONTHS[_is_leap(dt2_year)][dt2_month]; - - if (day_diff < days_in_month - days_in_last_month) - { - // We don't have a full month, we calculate days - if (days_in_last_month < dt1_day) - { - day_diff += dt1_day; - } - else - { - day_diff += days_in_last_month; - } - } - else if (day_diff == days_in_month - days_in_last_month) - { - // We have exactly a full month - // We remove the days difference - // and add one to the months difference - day_diff = 0; - month_diff += 1; - } - else - { - // We have a full month - day_diff += days_in_last_month; - } - - month_diff -= 1; - } - - if (month_diff < 0) - { - month_diff += 12; - year_diff -= 1; - } - - return new_diff( - year_diff * sign, - month_diff * sign, - day_diff * sign, - hour_diff * sign, - minute_diff * sign, - second_diff * sign, - microsecond_diff * sign, - total_days * sign); -} - -/* ------------------------------------------------------------------------- */ - -static PyMethodDef helpers_methods[] = { - {"is_leap", - (PyCFunction)is_leap, - METH_VARARGS, - PyDoc_STR("Checks if a year is a leap year.")}, - {"is_long_year", - (PyCFunction)is_long_year, - METH_VARARGS, - PyDoc_STR("Checks if a year is a long year.")}, - {"week_day", - (PyCFunction)week_day, - METH_VARARGS, - PyDoc_STR("Returns the weekday number.")}, - {"days_in_year", - (PyCFunction)days_in_year, - METH_VARARGS, - PyDoc_STR("Returns the number of days in the given year.")}, - {"timestamp", - (PyCFunction)timestamp, - METH_VARARGS, - PyDoc_STR("Returns the timestamp of the given datetime.")}, - {"local_time", - (PyCFunction)local_time, - METH_VARARGS, - PyDoc_STR("Returns a UNIX time as a broken down time for a particular transition type.")}, - {"precise_diff", - (PyCFunction)precise_diff, - METH_VARARGS, - PyDoc_STR("Calculate a precise difference between two datetimes.")}, - {NULL}}; - -/* ------------------------------------------------------------------------- */ - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_helpers", - NULL, - -1, - helpers_methods, - NULL, - NULL, - NULL, - NULL, -}; - -PyMODINIT_FUNC -PyInit__helpers(void) -{ - PyObject *module; - - PyDateTime_IMPORT; - - module = PyModule_Create(&moduledef); - - if (module == NULL) - return NULL; - - // Diff declaration - Diff_type.tp_new = PyType_GenericNew; - Diff_type.tp_members = Diff_members; - Diff_type.tp_init = (initproc)Diff_init; - - if (PyType_Ready(&Diff_type) < 0) - return NULL; - - PyModule_AddObject(module, "PreciseDiff", (PyObject *)&Diff_type); - - return module; -} diff --git a/pendulum/_extensions/_helpers.pyi b/pendulum/_extensions/_helpers.pyi deleted file mode 100644 index f7763557..00000000 --- a/pendulum/_extensions/_helpers.pyi +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from datetime import date -from datetime import datetime -from typing import NamedTuple - -def days_in_year(year: int) -> int: ... -def is_leap(year: int) -> bool: ... -def is_long_year(year: int) -> bool: ... -def local_time( - unix_time: int, utc_offset: int, microseconds: int -) -> tuple[int, int, int, int, int, int, int]: ... - -class PreciseDiff(NamedTuple): - years: int - months: int - days: int - hours: int - minutes: int - seconds: int - microseconds: int - total_days: int - -def precise_diff(d1: datetime | date, d2: datetime | date) -> PreciseDiff: ... -def timestamp(dt: datetime) -> int: ... -def week_day(year: int, month: int, day: int) -> int: ... diff --git a/pendulum/_extensions/helpers.py b/pendulum/_helpers.py similarity index 100% rename from pendulum/_extensions/helpers.py rename to pendulum/_helpers.py diff --git a/pendulum/helpers.py b/pendulum/helpers.py index 6b62fe77..e9390ca9 100644 --- a/pendulum/helpers.py +++ b/pendulum/helpers.py @@ -31,23 +31,21 @@ if not with_extensions or struct.calcsize("P") == 4: raise ImportError() - from pendulum._extensions._helpers import PreciseDiff - from pendulum._extensions._helpers import days_in_year - from pendulum._extensions._helpers import is_leap - from pendulum._extensions._helpers import is_long_year - from pendulum._extensions._helpers import local_time - from pendulum._extensions._helpers import precise_diff - from pendulum._extensions._helpers import timestamp - from pendulum._extensions._helpers import week_day + from _pendulum import PreciseDiff + from _pendulum import days_in_year + from _pendulum import is_leap + from _pendulum import is_long_year + from _pendulum import local_time + from _pendulum import precise_diff + from _pendulum import week_day except ImportError: - from pendulum._extensions.helpers import PreciseDiff # type: ignore[assignment] - from pendulum._extensions.helpers import days_in_year - from pendulum._extensions.helpers import is_leap - from pendulum._extensions.helpers import is_long_year - from pendulum._extensions.helpers import local_time - from pendulum._extensions.helpers import precise_diff # type: ignore[assignment] - from pendulum._extensions.helpers import timestamp - from pendulum._extensions.helpers import week_day + from pendulum._helpers import PreciseDiff # type: ignore[assignment] + from pendulum._helpers import days_in_year + from pendulum._helpers import is_leap + from pendulum._helpers import is_long_year + from pendulum._helpers import local_time + from pendulum._helpers import precise_diff # type: ignore[assignment] + from pendulum._helpers import week_day difference_formatter = DifferenceFormatter() @@ -211,7 +209,6 @@ def week_ends_at(wday: int) -> None: "is_long_year", "local_time", "precise_diff", - "timestamp", "week_day", "add_duration", "format_diff", diff --git a/pendulum/parsing/_iso8601.c b/pendulum/parsing/_iso8601.c deleted file mode 100644 index 1322423a..00000000 --- a/pendulum/parsing/_iso8601.c +++ /dev/null @@ -1,1361 +0,0 @@ -/* ------------------------------------------------------------------------- */ - -#include -#include -#include -#include -#include -#include -#include - -#ifndef PyVarObject_HEAD_INIT -#define PyVarObject_HEAD_INIT(type, size) PyObject_HEAD_INIT(type) size, -#endif - - -/* ------------------------------------------------------------------------- */ - -#define EPOCH_YEAR 1970 - -#define DAYS_PER_N_YEAR 365 -#define DAYS_PER_L_YEAR 366 - -#define USECS_PER_SEC 1000000 - -#define SECS_PER_MIN 60 -#define SECS_PER_HOUR (60 * SECS_PER_MIN) -#define SECS_PER_DAY (SECS_PER_HOUR * 24) - -// 400-year chunks always have 146097 days (20871 weeks). -#define DAYS_PER_400_YEARS 146097L -#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY) - -// 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. -const int64_t SECS_PER_100_YEARS[2] = { - (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY -}; - -// 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. -const int32_t SECS_PER_4_YEARS[2] = { - (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY -}; - -// The number of seconds in non-leap and leap years respectively. -const int32_t SECS_PER_YEAR[2] = { - DAYS_PER_N_YEAR * SECS_PER_DAY, - DAYS_PER_L_YEAR * SECS_PER_DAY -}; - -#define MONTHS_PER_YEAR 12 - -// The month lengths in non-leap and leap years respectively. -const int32_t DAYS_PER_MONTHS[2][13] = { - {-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. -const int32_t MONTHS_OFFSETS[2][14] = { - {-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} -}; - -const int DAY_OF_WEEK_TABLE[12] = { - 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 -}; - -#define TM_SUNDAY 0 -#define TM_MONDAY 1 -#define TM_TUESDAY 2 -#define TM_WEDNESDAY 3 -#define TM_THURSDAY 4 -#define TM_FRIDAY 5 -#define TM_SATURDAY 6 - -#define TM_JANUARY 0 -#define TM_FEBRUARY 1 -#define TM_MARCH 2 -#define TM_APRIL 3 -#define TM_MAY 4 -#define TM_JUNE 5 -#define TM_JULY 6 -#define TM_AUGUST 7 -#define TM_SEPTEMBER 8 -#define TM_OCTOBER 9 -#define TM_NOVEMBER 10 -#define TM_DECEMBER 11 - -// Parsing errors -const int PARSER_INVALID_ISO8601 = 0; -const int PARSER_INVALID_DATE = 1; -const int PARSER_INVALID_TIME = 2; -const int PARSER_INVALID_WEEK_DATE = 3; -const int PARSER_INVALID_WEEK_NUMBER = 4; -const int PARSER_INVALID_WEEKDAY_NUMBER = 5; -const int PARSER_INVALID_ORDINAL_DAY_FOR_YEAR = 6; -const int PARSER_INVALID_MONTH_OR_DAY = 7; -const int PARSER_INVALID_MONTH = 8; -const int PARSER_INVALID_DAY_FOR_MONTH = 9; -const int PARSER_INVALID_HOUR = 10; -const int PARSER_INVALID_MINUTE = 11; -const int PARSER_INVALID_SECOND = 12; -const int PARSER_INVALID_SUBSECOND = 13; -const int PARSER_INVALID_TZ_OFFSET = 14; -const int PARSER_INVALID_DURATION = 15; -const int PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED = 16; - -const char PARSER_ERRORS[17][80] = { - "Invalid ISO 8601 string", - "Invalid date", - "Invalid time", - "Invalid week date", - "Invalid week number", - "Invalid weekday number", - "Invalid ordinal day for year", - "Invalid month and/or day", - "Invalid month", - "Invalid day for month", - "Invalid hour", - "Invalid minute", - "Invalid second", - "Invalid subsecond", - "Invalid timezone offset", - "Invalid duration", - "Float years and months are not supported" -}; - -/* ------------------------------------------------------------------------- */ - - -int p(int y) { - return y + y/4 - y/100 + y/400; -} - -int is_leap(int year) { - return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); -} - -int week_day(int year, int month, int day) { - int y; - int w; - - y = year - (month < 3); - - w = (p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7; - - if (!w) { - w = 7; - } - - return w; -} - -int days_in_year(int year) { - if (is_leap(year)) { - return DAYS_PER_L_YEAR; - } - - return DAYS_PER_N_YEAR; -} - -int is_long_year(int year) { - return (p(year) % 7 == 4) || (p(year - 1) % 7 == 3); -} - - -/* ------------------------ Custom Types ------------------------------- */ - - -/* - * class FixedOffset(tzinfo): - */ -typedef struct { - PyObject_HEAD - int offset; - char *tzname; -} FixedOffset; - -/* - * def __init__(self, offset): - * self.offset = offset -*/ -static int FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs) { - int offset; - char *tzname = NULL; - - static char *kwlist[] = {"offset", "tzname", NULL}; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|s", kwlist, &offset, &tzname)) - return -1; - - self->offset = offset; - self->tzname = tzname; - - return 0; -} - -/* - * def utcoffset(self, dt): - * return timedelta(seconds=self.offset * 60) - */ -static PyObject *FixedOffset_utcoffset(FixedOffset *self, PyObject *args) { - return PyDelta_FromDSU(0, self->offset, 0); -} - -/* - * def dst(self, dt): - * return timedelta(seconds=self.offset * 60) - */ -static PyObject *FixedOffset_dst(FixedOffset *self, PyObject *args) { - return PyDelta_FromDSU(0, self->offset, 0); -} - -/* - * def tzname(self, dt): - * sign = '+' - * if self.offset < 0: - * sign = '-' - * return f"{sign}{self.offset / 60}:{self.offset % 60}" - */ -static PyObject *FixedOffset_tzname(FixedOffset *self, PyObject *args) { - if (self->tzname != NULL) { - return PyUnicode_FromString(self->tzname); - } - - char sign = '+'; - int offset = self->offset; - - if (offset < 0) { - sign = '-'; - offset *= -1; - } - - return PyUnicode_FromFormat( - "%c%02d:%02d", - sign, - offset / SECS_PER_HOUR, - offset / SECS_PER_MIN % SECS_PER_MIN - ); -} - -/* - * def __repr__(self): - * return self.tzname() - */ -static PyObject *FixedOffset_repr(FixedOffset *self) { - return FixedOffset_tzname(self, NULL); -} - -/* - * Class member / class attributes - */ -static PyMemberDef FixedOffset_members[] = { - {"offset", T_INT, offsetof(FixedOffset, offset), 0, "UTC offset"}, - {NULL} -}; - -/* - * Class methods - */ -static PyMethodDef FixedOffset_methods[] = { - {"utcoffset", (PyCFunction)FixedOffset_utcoffset, METH_VARARGS, ""}, - {"dst", (PyCFunction)FixedOffset_dst, METH_VARARGS, ""}, - {"tzname", (PyCFunction)FixedOffset_tzname, METH_VARARGS, ""}, - {NULL} -}; - -static PyTypeObject FixedOffset_type = { - PyVarObject_HEAD_INIT(NULL, 0) - "FixedOffset_type", /* tp_name */ - sizeof(FixedOffset), /* tp_basicsize */ - 0, /* tp_itemsize */ - 0, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_as_async */ - (reprfunc)FixedOffset_repr, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - (reprfunc)FixedOffset_repr, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */ - "TZInfo with fixed offset", /* tp_doc */ -}; - -/* - * Instantiate new FixedOffset_type object - * Skip overhead of calling PyObject_New and PyObject_Init. - * Directly allocate object. - */ -static PyObject *new_fixed_offset_ex(int offset, char *name, PyTypeObject *type) { - FixedOffset *self = (FixedOffset *) (type->tp_alloc(type, 0)); - - if (self != NULL) { - self->offset = offset; - self->tzname = name; - } - - return (PyObject *) self; -} - -#define new_fixed_offset(offset, name) new_fixed_offset_ex(offset, name, &FixedOffset_type) - - -/* - * class Duration(): - */ -typedef struct { - PyObject_HEAD - int years; - int months; - int weeks; - int days; - int hours; - int minutes; - int seconds; - int microseconds; -} Duration; - -/* - * def __init__(self, years, months, days, hours, minutes, seconds, microseconds): - * self.years = years - * # ... -*/ -static int Duration_init(Duration *self, PyObject *args, PyObject *kwargs) { - int years; - int months; - int weeks; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - - if (!PyArg_ParseTuple(args, "iiiiiiii", &years, &months, &weeks, &days, &hours, &minutes, &seconds, µseconds)) - return -1; - - self->years = years; - self->months = months; - self->weeks = weeks; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - - return 0; -} - -/* - * def __repr__(self): - * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format( - * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds - * ) - */ -static PyObject *Duration_repr(Duration *self) { - return PyUnicode_FromFormat( - "%d years %d months %d weeks %d days %d hours %d minutes %d seconds %d microseconds", - self->years, - self->months, - self->weeks, - self->days, - self->hours, - self->minutes, - self->seconds, - self->microseconds - ); -} - -/* - * Instantiate new Duration_type object - * Skip overhead of calling PyObject_New and PyObject_Init. - * Directly allocate object. - */ -static PyObject *new_duration_ex(int years, int months, int weeks, int days, int hours, int minutes, int seconds, int microseconds, PyTypeObject *type) { - Duration *self = (Duration *) (type->tp_alloc(type, 0)); - - if (self != NULL) { - self->years = years; - self->months = months; - self->weeks = weeks; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - } - - return (PyObject *) self; -} - -/* - * Class member / class attributes - */ -static PyMemberDef Duration_members[] = { - {"years", T_INT, offsetof(Duration, years), 0, "years in duration"}, - {"months", T_INT, offsetof(Duration, months), 0, "months in duration"}, - {"weeks", T_INT, offsetof(Duration, weeks), 0, "weeks in duration"}, - {"days", T_INT, offsetof(Duration, days), 0, "days in duration"}, - {"remaining_days", T_INT, offsetof(Duration, days), 0, "days in duration"}, - {"hours", T_INT, offsetof(Duration, hours), 0, "hours in duration"}, - {"minutes", T_INT, offsetof(Duration, minutes), 0, "minutes in duration"}, - {"seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"}, - {"remaining_seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"}, - {"microseconds", T_INT, offsetof(Duration, microseconds), 0, "microseconds in duration"}, - {NULL} -}; - -static PyTypeObject Duration_type = { - PyVarObject_HEAD_INIT(NULL, 0) - "Duration", /* tp_name */ - sizeof(Duration), /* tp_basicsize */ - 0, /* tp_itemsize */ - 0, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_as_async */ - (reprfunc)Duration_repr, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - (reprfunc)Duration_repr, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */ - "Duration", /* tp_doc */ -}; - -#define new_duration(years, months, weeks, days, hours, minutes, seconds, microseconds) new_duration_ex(years, months, weeks, days, hours, minutes, seconds, microseconds, &Duration_type) - -typedef struct { - int is_date; - int is_time; - int is_datetime; - int is_duration; - int is_period; - int ambiguous; - int year; - int month; - int day; - int hour; - int minute; - int second; - int microsecond; - int offset; - int has_offset; - char *tzname; - int years; - int months; - int weeks; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - int error; -} Parsed; - - -Parsed* new_parsed() { - Parsed *parsed; - - if((parsed = malloc(sizeof *parsed)) != NULL) { - parsed->is_date = 0; - parsed->is_time = 0; - parsed->is_datetime = 0; - parsed->is_duration = 0; - parsed->is_period = 0; - - parsed->ambiguous = 0; - parsed->year = 0; - parsed->month = 1; - parsed->day = 1; - parsed->hour = 0; - parsed->minute = 0; - parsed->second = 0; - parsed->microsecond = 0; - parsed->offset = 0; - parsed->has_offset = 0; - parsed->tzname = NULL; - - parsed->years = 0; - parsed->months = 0; - parsed->weeks = 0; - parsed->days = 0; - parsed->hours = 0; - parsed->minutes = 0; - parsed->seconds = 0; - parsed->microseconds = 0; - - parsed->error = -1; - } - - return parsed; -} - - -/* -------------------------- Functions --------------------------*/ - -Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) { - char* c; - int monthday = 0; - int week = 0; - int weekday = 1; - int ordinal; - int tz_sign = 0; - int leap = 0; - int separators = 0; - int time = 0; - int i; - int j; - - // Assuming date only for now - parsed->is_date = 1; - - c = str; - - for (i = 0; i < 4; i++) { - if (*c >= '0' && *c <= '9') { - parsed->year = 10 * parsed->year + *c++ - '0'; - } else { - parsed->error = PARSER_INVALID_ISO8601; - - return NULL; - } - } - - leap = is_leap(parsed->year); - - // Optional separator - if (*c == '-') { - separators++; - c++; - } - - // Checking for week dates - if (*c == 'W') { - c++; - - i = 0; - while (*c != '\0' && *c != ' ' && *c != 'T') { - if (*c == '-') { - separators++; - c++; - continue; - } - - week = 10 * week + *c++ - '0'; - - i++; - } - - switch (i) { - case 2: - // Only week number - break; - case 3: - // Week with weekday - if (!(separators == 0 || separators == 2)) { - // We should have 2 or no separator - parsed->error = PARSER_INVALID_WEEK_DATE; - - return NULL; - } - - weekday = week % 10; - week /= 10; - - break; - default: - // Any other case is wrong - parsed->error = PARSER_INVALID_WEEK_DATE; - - return NULL; - } - - // Checks - if (week > 53 || (week > 52 && !is_long_year(parsed->year))) { - parsed->error = PARSER_INVALID_WEEK_NUMBER; - - return NULL; - } - - if (weekday > 7) { - parsed->error = PARSER_INVALID_WEEKDAY_NUMBER; - - return NULL; - } - - // Calculating ordinal day - ordinal = week * 7 + weekday - (week_day(parsed->year, 1, 4) + 3); - - if (ordinal < 1) { - // Previous year - ordinal += days_in_year(parsed->year - 1); - parsed->year -= 1; - leap = is_leap(parsed->year); - } - - if (ordinal > days_in_year(parsed->year)) { - // Next year - ordinal -= days_in_year(parsed->year); - parsed->year += 1; - leap = is_leap(parsed->year); - } - - for (j = 1; j < 14; j++) { - if (ordinal <= MONTHS_OFFSETS[leap][j]) { - parsed->day = ordinal - MONTHS_OFFSETS[leap][j - 1]; - parsed->month = j - 1; - - break; - } - } - } else { - // At this point we need to check the number - // of characters until the end of the date part - // (or the end of the string). - // - // If two, we have only a month if there is a separator, it may be a time otherwise. - // If three, we have an ordinal date. - // If four, we have a complete date - i = 0; - while (*c != '\0' && *c != ' ' && *c != 'T') { - if (*c == '-') { - separators++; - c++; - continue; - } - - if (!(*c >= '0' && *c <='9')) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - monthday = 10 * monthday + *c++ - '0'; - - i++; - } - - switch (i) { - case 0: - // No month/day specified (only a year) - break; - case 2: - if (!separators) { - // The date looks like 201207 - // which is invalid for a date - // But it might be a time in the form hhmmss - parsed->ambiguous = 1; - } else if (separators > 1) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - parsed->month = monthday; - break; - case 3: - // Ordinal day - if (separators > 1) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - if (monthday < 1 || monthday > MONTHS_OFFSETS[leap][13]) { - parsed->error = PARSER_INVALID_ORDINAL_DAY_FOR_YEAR; - - return NULL; - } - - for (j = 1; j < 14; j++) { - if (monthday <= MONTHS_OFFSETS[leap][j]) { - parsed->day = monthday - MONTHS_OFFSETS[leap][j - 1]; - parsed->month = j - 1; - - break; - } - } - - break; - case 4: - // Month and day - parsed->month = monthday / 100; - parsed->day = monthday % 100; - - break; - default: - parsed->error = PARSER_INVALID_MONTH_OR_DAY; - - return NULL; - } - } - - // Checks - if (separators && !monthday && !week) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - if (parsed->month > 12) { - parsed->error = PARSER_INVALID_MONTH; - - return NULL; - } - - if (parsed->day > DAYS_PER_MONTHS[leap][parsed->month]) { - parsed->error = PARSER_INVALID_DAY_FOR_MONTH; - - return NULL; - } - - separators = 0; - if (*c == 'T' || *c == ' ') { - if (parsed->ambiguous) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - // We have time so we have a datetime - parsed->is_datetime = 1; - parsed->is_date = 0; - - c++; - - // Grabbing time information - i = 0; - while (*c != '\0' && *c != '.' && *c != ',' && *c != 'Z' && *c != '+' && *c != '-') { - if (*c == ':') { - separators++; - c++; - continue; - } - - if (!(*c >= '0' && *c <='9')) { - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - time = 10 * time + *c++ - '0'; - i++; - } - - switch (i) { - case 2: - // Hours only - if (separators > 0) { - // Extraneous separators - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - parsed->hour = time; - break; - case 4: - // Hours and minutes - if (separators > 1) { - // Extraneous separators - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - parsed->hour = time / 100; - parsed->minute = time % 100; - break; - case 6: - // Hours, minutes and seconds - if (!(separators == 0 || separators == 2)) { - // We should have either two separators or none - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - parsed->hour = time / 10000; - parsed->minute = time / 100 % 100; - parsed->second = time % 100; - break; - default: - // Any other case is wrong - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - // Checks - if (parsed->hour > 23) { - parsed->error = PARSER_INVALID_HOUR; - - return NULL; - } - - if (parsed->minute > 59) { - parsed->error = PARSER_INVALID_MINUTE; - - return NULL; - } - - if (parsed->second > 59) { - parsed->error = PARSER_INVALID_SECOND; - - return NULL; - } - - // Subsecond - if (*c == '.' || *c == ',') { - c++; - - time = 0; - i = 0; - while (*c != '\0' && *c != 'Z' && *c != '+' && *c != '-') { - if (!(*c >= '0' && *c <='9')) { - parsed->error = PARSER_INVALID_SUBSECOND; - - return NULL; - } - - time = 10 * time + *c++ - '0'; - i++; - } - - // adjust to microseconds - if (i > 6) { - parsed->microsecond = time / pow(10, i - 6); - } else if (i <= 6) { - parsed->microsecond = time * pow(10, 6 - i); - } - } - - // Timezone - if (*c == 'Z') { - parsed->has_offset = 1; - parsed->tzname = "UTC"; - c++; - } else if (*c == '+' || *c == '-') { - tz_sign = 1; - if (*c == '-') { - tz_sign = -1; - } - - parsed->has_offset = 1; - c++; - - i = 0; - time = 0; - separators = 0; - while (*c != '\0') { - if (*c == ':') { - separators++; - c++; - continue; - } - - if (!(*c >= '0' && *c <= '9')) { - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - - time = 10 * time + *c++ - '0'; - i++; - } - - switch (i) { - case 2: - // hh Format - if (separators) { - // Extraneous separators - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - - parsed->offset = tz_sign * (time * 3600); - break; - case 4: - // hhmm Format - if (separators > 1) { - // Extraneous separators - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - - parsed->offset = tz_sign * ((time / 100 * 3600) + (time % 100 * 60)); - break; - default: - // Wrong format - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - } - } - - // At this point we should be at the end of the string - // If not, the string is invalid - if (*c != '\0') { - parsed->error = PARSER_INVALID_ISO8601; - - return NULL; - } - - return parsed; -} - - -Parsed* _parse_iso8601_duration(char *str, Parsed *parsed) { - char* c; - int value = 0; - int grabbed = 0; - int in_time = 0; - int in_fraction = 0; - int fraction_length = 0; - int has_fractional = 0; - int fraction = 0; - int has_ymd = 0; - int has_week = 0; - int has_month = 0; - int has_day = 0; - int has_hour = 0; - int has_minute = 0; - int has_second = 0; - - c = str; - - // Removing P operator - c++; - - parsed->is_duration = 1; - - for (; *c != '\0'; c++) { - switch (*c) { - case 'Y': - if (!grabbed || in_time || has_week || has_ymd) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (fraction) { - parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED; - - return NULL; - } - - parsed->years = value; - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_ymd = 1; - - break; - case 'M': - if (!grabbed || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (in_time) { - if (has_second) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_fractional) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->minutes = value; - if (fraction) { - parsed->seconds = fraction * 6; - has_fractional = 1; - } - - has_minute = 1; - } else { - if (fraction) { - parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED; - - return NULL; - } - - if (has_month || has_day) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->months = value; - has_ymd = 1; - has_month = 1; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - - break; - case 'D': - if (!grabbed || in_time || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_day) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->days = value; - if (fraction) { - parsed->hours = fraction * 2.4; - has_fractional = 1; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_ymd = 1; - has_day = 1; - - break; - case 'T': - if (grabbed) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - in_time = 1; - - break; - case 'H': - if (!grabbed || !in_time || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_hour || has_second || has_minute) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_fractional) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->hours = value; - if (fraction) { - parsed->minutes = fraction * 6; - has_fractional = 1; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_hour = 1; - - break; - case 'S': - if (!grabbed || !in_time || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_second) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_fractional) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (fraction) { - parsed->seconds = value; - if (fraction_length > 6) { - parsed->microseconds = fraction / pow(10, fraction_length - 6); - } else { - parsed->microseconds = fraction * pow(10, 6 - fraction_length); - } - has_fractional = 1; - } else { - parsed->seconds = value; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_second = 1; - - break; - case 'W': - if (!grabbed || in_time || has_ymd) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->weeks = value; - if (fraction) { - float days; - days = fraction * 0.7; - parsed->hours = (int) ((days - (int) days) * 24); - parsed->days = (int) days; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_week = 1; - - break; - case '.': - if (!grabbed || has_fractional) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - in_fraction = 1; - - break; - case ',': - if (!grabbed || has_fractional) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - in_fraction = 1; - - break; - default: - if (*c >= '0' && *c <='9') { - if (in_fraction) { - fraction = 10 * fraction + *c - '0'; - fraction_length++; - } else { - value = 10 * value + *c - '0'; - grabbed = 1; - } - break; - } - - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - } - - return parsed; -} - - -PyObject* parse_iso8601(PyObject *self, PyObject *args) { - char* str; - PyObject *obj; - PyObject *tzinfo; - Parsed *parsed = new_parsed(); - - if (!PyArg_ParseTuple(args, "s", &str)) { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters" - ); - free(parsed); - return NULL; - } - - if (*str == 'P') { - // Duration (or interval) - if (_parse_iso8601_duration(str, parsed) == NULL) { - PyErr_SetString( - PyExc_ValueError, PARSER_ERRORS[parsed->error] - ); - - free(parsed); - return NULL; - } - } else if (_parse_iso8601_datetime(str, parsed) == NULL) { - PyErr_SetString( - PyExc_ValueError, PARSER_ERRORS[parsed->error] - ); - - free(parsed); - return NULL; - } - - if (parsed->is_date) { - // Date only - if (parsed->ambiguous) { - // We can "safely" assume that the ambiguous - // date was actually a time in the form hhmmss - parsed->hour = parsed->year / 100; - parsed->minute = parsed->year % 100; - parsed->second = parsed->month; - - obj = PyDateTimeAPI->Time_FromTime( - parsed->hour, parsed->minute, parsed->second, parsed->microsecond, - Py_BuildValue(""), - PyDateTimeAPI->TimeType - ); - } else { - obj = PyDateTimeAPI->Date_FromDate( - parsed->year, parsed->month, parsed->day, - PyDateTimeAPI->DateType - ); - } - } else if (parsed->is_datetime) { - if (!parsed->has_offset) { - tzinfo = Py_BuildValue(""); - } else { - tzinfo = new_fixed_offset(parsed->offset, parsed->tzname); - } - - obj = PyDateTimeAPI->DateTime_FromDateAndTime( - parsed->year, - parsed->month, - parsed->day, - parsed->hour, - parsed->minute, - parsed->second, - parsed->microsecond, - tzinfo, - PyDateTimeAPI->DateTimeType - ); - - Py_DECREF(tzinfo); - } else if (parsed->is_duration) { - obj = new_duration( - parsed->years, parsed->months, parsed->weeks, parsed->days, - parsed->hours, parsed->minutes, parsed->seconds, parsed->microseconds - ); - } else { - free(parsed); - return NULL; - } - - free(parsed); - - return obj; -} - - -/* ------------------------------------------------------------------------- */ - -static PyMethodDef helpers_methods[] = { - { - "parse_iso8601", - (PyCFunction) parse_iso8601, - METH_VARARGS, - PyDoc_STR("Parses a ISO8601 string into a tuple.") - }, - {NULL} -}; - - -/* ------------------------------------------------------------------------- */ - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_iso8601", - NULL, - -1, - helpers_methods, - NULL, - NULL, - NULL, - NULL, -}; - -PyMODINIT_FUNC -PyInit__iso8601(void) -{ - PyObject *module; - - PyDateTime_IMPORT; - - module = PyModule_Create(&moduledef); - - if (module == NULL) - return NULL; - - // FixedOffset declaration - FixedOffset_type.tp_new = PyType_GenericNew; - FixedOffset_type.tp_base = PyDateTimeAPI->TZInfoType; - FixedOffset_type.tp_methods = FixedOffset_methods; - FixedOffset_type.tp_members = FixedOffset_members; - FixedOffset_type.tp_init = (initproc)FixedOffset_init; - - if (PyType_Ready(&FixedOffset_type) < 0) - return NULL; - - // Duration declaration - Duration_type.tp_new = PyType_GenericNew; - Duration_type.tp_members = Duration_members; - Duration_type.tp_init = (initproc)Duration_init; - - if (PyType_Ready(&Duration_type) < 0) - return NULL; - - Py_INCREF(&FixedOffset_type); - Py_INCREF(&Duration_type); - - PyModule_AddObject(module, "TZFixedOffset", (PyObject *)&FixedOffset_type); - PyModule_AddObject(module, "Duration", (PyObject *)&Duration_type); - - return module; -} diff --git a/pyproject.toml b/pyproject.toml index 1b30fc07..e0382f98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,5 +224,5 @@ omit = [ ] [build-system] -requires = ["poetry-core>=1.1.0a6", "meson", "ninja", "maturin>=1,<2"] +requires = ["poetry-core>=1.1.0a6", "maturin>=1,<2"] build-backend = "poetry.core.masonry.api" diff --git a/rust/helpers.rs b/rust/helpers.rs index 55a5c09d..5edbeb57 100644 --- a/rust/helpers.rs +++ b/rust/helpers.rs @@ -4,19 +4,19 @@ use crate::constants::{ SECS_PER_MIN, SECS_PER_YEAR, TM_DECEMBER, TM_JANUARY, }; -fn p(year: u32) -> u32 { +fn p(year: i32) -> i32 { return year + year / 4 - year / 100 + year / 400; } -pub fn is_leap(year: u32) -> bool { +pub fn is_leap(year: i32) -> bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) } -pub fn is_long_year(year: u32) -> bool { +pub fn is_long_year(year: i32) -> bool { (p(year) % 7 == 4) || (p(year - 1) % 7 == 3) } -pub fn days_in_year(year: u32) -> u32 { +pub fn days_in_year(year: i32) -> u32 { if is_leap(year) { return DAYS_PER_L_YEAR; } @@ -24,25 +24,32 @@ pub fn days_in_year(year: u32) -> u32 { DAYS_PER_N_YEAR } -pub fn week_day(year: u32, month: u32, day: u32) -> u32 { - let y: u32 = year - (month < 3) as u32; +pub fn week_day(year: i32, month: u32, day: u32) -> u32 { + let y: i32 = year - (month < 3) as i32; - let w: u32 = (p(y) + DAY_OF_WEEK_TABLE[(month - 1) as usize] + day) % 7; + 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 + w.abs() as u32 +} + +pub fn day_number(year: i32, month: u8, day: u8) -> i32 { + let m = ((month + 9) % 12) as i32; + let y = year - m / 10; + + return 365 * y + y / 4 - y / 100 + y / 400 + (m * 306 + 5) / 10 + (day as i32 - 1); } pub fn local_time( - unix_time: isize, + 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; + let mut seconds: isize = unix_time.floor() as isize; // Shift to a base year that is 400-year aligned. if seconds >= 0 { diff --git a/rust/parsing.rs b/rust/parsing.rs index 65f850c9..ce5803ee 100644 --- a/rust/parsing.rs +++ b/rust/parsing.rs @@ -875,7 +875,7 @@ impl<'a> Parser<'a> { iso_week: u32, iso_day: u32, ) -> Result<(u32, u32, u32), ParseError> { - if iso_week > 53 || iso_week > 52 && !is_long_year(iso_year) { + if iso_week > 53 || iso_week > 52 && !is_long_year(iso_year as i32) { return Err(ParseError { index: self.idx, c: self.current, @@ -895,7 +895,7 @@ impl<'a> Parser<'a> { } let ordinal: i32 = - iso_week as i32 * 7 + iso_day as i32 - (week_day(iso_year, 1, 4) as i32 + 3); + 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) } @@ -908,9 +908,7 @@ impl<'a> Parser<'a> { ) -> Result<(u32, u32, u32), ParseError> { let mut ord: i32 = ordinal; let mut y: u32 = year; - let mut leap: usize = is_leap(y) as usize; - let mut month: u32 = 1; - let mut day: u32 = 1; + let mut leap: usize = is_leap(y as i32) as usize; if ord < 1 { if !allow_out_of_bounds { @@ -921,12 +919,12 @@ impl<'a> Parser<'a> { ))); } // Previous year - ord += days_in_year(year - 1) as i32; + ord += days_in_year((year - 1) as i32) as i32; y -= 1; - leap = is_leap(y) as usize; + leap = is_leap(y as i32) as usize; } - if ord > days_in_year(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: {} is too large for year {}", @@ -936,15 +934,15 @@ impl<'a> Parser<'a> { } // Next year - ord -= days_in_year(y) as i32; + ord -= days_in_year(y as i32) as i32; y += 1; - leap = is_leap(y) as usize; + leap = is_leap(y as i32) as usize; } for i in 1..14 { if ord < MONTHS_OFFSETS[leap][i] { - day = ord as u32 - MONTHS_OFFSETS[leap][i - 1] as u32; - month = (i - 1) as u32; + let day = ord as u32 - MONTHS_OFFSETS[leap][i - 1] as u32; + let month = (i - 1) as u32; return Ok((y as u32, month, day)); } diff --git a/rust/python/helpers.rs b/rust/python/helpers.rs index 449afefe..6ac33be5 100644 --- a/rust/python/helpers.rs +++ b/rust/python/helpers.rs @@ -1,27 +1,382 @@ -use pyo3::prelude::*; +use pyo3::{ + intern, + prelude::*, + types::{PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyString, PyTimeAccess}, + PyTypeInfo, +}; -use crate::helpers; +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 { + return ( + 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 { + return ( + 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(_) => return 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(); + } else { + return Ok(tz); + } + } + } +} + +pub fn get_offset<'py>(dt: &'py 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()?; + + return Ok(offset.get_days() * SECS_PER_DAY as i32 + offset.get_seconds()); +} #[pyfunction] -pub fn is_leap(year: u32) -> PyResult { +pub fn is_leap(year: i32) -> PyResult { Ok(helpers::is_leap(year)) } #[pyfunction] -pub fn is_long_year(year: u32) -> PyResult { +pub fn is_long_year(year: i32) -> PyResult { Ok(helpers::is_long_year(year)) } #[pyfunction] -pub fn week_day(year: u32, month: u32, day: u32) -> PyResult { +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: isize, + 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: dt1.downcast::()?.get_month() as i32, + day: dt1.downcast::()?.get_day() as i32, + 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: dt2.downcast::()?.get_month() as i32, + day: dt2.downcast::()?.get_day() as i32, + 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.len() > 0; + 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 = dt1dt.get_hour() as i32; + dtinfo1.minute = dt1dt.get_minute() as i32; + dtinfo1.second = dt1dt.get_second() as i32; + 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 = dt2dt.get_hour() as i32; + dtinfo2.minute = dt2dt.get_minute() as i32; + dtinfo2.second = dt2dt.get_second() as i32; + 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 += 1000000; + 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[leap as usize][month as usize]; + let days_in_month = + DAYS_PER_MONTHS[helpers::is_leap(dtinfo2.year) as usize][dtinfo2.month as usize]; + + if day_diff < days_in_month - days_in_last_month { + // 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; + } + } else if day_diff == days_in_month - days_in_last_month { + // We have exactly a full month + // We remove the days difference + // and add one to the months difference + day_diff = 0; + month_diff += 1; + } else { + // We have a full month + day_diff += days_in_last_month; + } + + month_diff -= 1; + } + + if month_diff < 0 { + month_diff += 12; + year_diff -= 1; + } + + return 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/python/mod.rs b/rust/python/mod.rs index d0bf657f..8d3cd41a 100644 --- a/rust/python/mod.rs +++ b/rust/python/mod.rs @@ -4,18 +4,21 @@ mod helpers; mod parsing; mod types; -use helpers::{is_leap, is_long_year, local_time, week_day}; +use helpers::{days_in_year, is_leap, is_long_year, local_time, precise_diff, week_day}; use parsing::parse_iso8601; -use types::Duration; +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__ diff --git a/rust/python/types/mod.rs b/rust/python/types/mod.rs index 236f6e37..cba11dfe 100644 --- a/rust/python/types/mod.rs +++ b/rust/python/types/mod.rs @@ -1,5 +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/python/types/precise_diff.rs b/rust/python/types/precise_diff.rs new file mode 100644 index 00000000..c9f67f9c --- /dev/null +++ b/rust/python/types/precise_diff.rs @@ -0,0 +1,52 @@ +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))] + 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/tests/test_helpers.py b/tests/test_helpers.py index 7756a063..b9ab63f8 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -48,6 +48,7 @@ def test_precise_diff() -> None: diff = precise_diff(dt1, dt2) assert_diff(diff, months=-1, seconds=-1) + assert diff.total_days == -30 diff = precise_diff(dt2, dt1) assert_diff(diff, months=1, seconds=1) @@ -93,11 +94,13 @@ def test_precise_diff_timezone() -> None: diff = precise_diff(dt1, dt2) assert_diff(diff, days=1, hours=0) + assert diff.total_days == 1 dt2 = toronto.datetime(2013, 4, 1, 1, 30) diff = precise_diff(dt1, dt2) assert_diff(diff, days=1, hours=5) + assert diff.total_days == 1 # pytz paris_pytz = pytz.timezone("Europe/Paris") @@ -108,6 +111,7 @@ def test_precise_diff_timezone() -> None: diff = precise_diff(dt1, dt2) assert_diff(diff, days=1, hours=0) + assert diff.total_days == 1 dt2 = toronto_pytz.localize(datetime(2013, 4, 1, 1, 30)) @@ -119,6 +123,7 @@ def test_precise_diff_timezone() -> None: dt2 = timezone("Europe/Paris").datetime(2018, 6, 20, 3, 40) # UTC+2 diff = precise_diff(dt1, dt2) assert_diff(diff, minutes=10) + assert diff.total_days == 0 def test_week_day() -> None: From aedd96a6fcee33d6adb134033ed5273bde825c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Thu, 3 Aug 2023 00:33:48 +0200 Subject: [PATCH 14/19] Fix parsing without extensions --- pendulum/parser.py | 5 ++++- pendulum/parsing/iso8601.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pendulum/parser.py b/pendulum/parser.py index 77f43752..66e33ea1 100644 --- a/pendulum/parser.py +++ b/pendulum/parser.py @@ -5,6 +5,7 @@ import pendulum +from pendulum.duration import Duration from pendulum.parsing import _Interval from pendulum.parsing import parse as base_parse from pendulum.tz.timezone import UTC @@ -13,7 +14,6 @@ if t.TYPE_CHECKING: from pendulum.date import Date from pendulum.datetime import DateTime - from pendulum.duration import Duration from pendulum.interval import Interval from pendulum.time import Time @@ -110,6 +110,9 @@ def _parse(text: str, **options: t.Any) -> Date | DateTime | Time | Duration | I ), ) + if isinstance(parsed, Duration): + return parsed + if CDuration and isinstance(parsed, CDuration): # type: ignore[truthy-function] return pendulum.duration( years=parsed.years, diff --git a/pendulum/parsing/iso8601.py b/pendulum/parsing/iso8601.py index 4f991755..cc4dd7aa 100644 --- a/pendulum/parsing/iso8601.py +++ b/pendulum/parsing/iso8601.py @@ -185,7 +185,7 @@ def parse_iso8601( if ambiguous_date: # We can "safely" assume that the ambiguous date # was actually a time in the form hhmmss - hhmmss = f"{year!s}{str(month):0>2}" + hhmmss = f"{year!s}{month!s:0>2}" return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:])) @@ -255,7 +255,7 @@ def parse_iso8601( tzinfo = FixedTimezone(offset) if is_time: - return datetime.time(hour, minute, second, microsecond) + return datetime.time(hour, minute, second, microsecond, tzinfo=tzinfo) return datetime.datetime( year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo From d248e4fe4f0499c2c63d0274c0ed81828c86dc1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Sat, 5 Aug 2023 00:13:17 +0200 Subject: [PATCH 15/19] Lint Rust code --- .github/workflows/tests.yml | 14 ++ .pre-commit-config.yaml | 9 ++ Makefile | 68 ++++++++++ rust/constants.rs | 27 +--- rust/helpers.rs | 16 +-- rust/parsing.rs | 210 ++++++++++++++---------------- rust/python/helpers.rs | 86 ++++++------ rust/python/parsing.rs | 62 ++++----- rust/python/types/duration.rs | 1 + rust/python/types/precise_diff.rs | 1 + rust/python/types/timezone.rs | 4 +- 11 files changed, 282 insertions(+), 216 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ddbb21a2..e8c604f3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,6 +13,20 @@ on: - '**' jobs: + Linting: + name: Linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: "Install pre-commit" + run: pip install pre-commit + - name: "Install Rust toolchain" + run: rustup component add rustfmt clippy + - run: pre-commit run --all-files + Tests: name: ${{ matrix.os }} / ${{ matrix.python-version }} runs-on: ${{ matrix.os }}-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2974fac7..ff460a86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,3 +20,12 @@ repos: rev: v0.0.270 hooks: - id: ruff + + - repo: local + hooks: + - id: lint-rust + name: Lint Rust + entry: make lint-rust + types: [rust] + language: rust + pass_filenames: false diff --git a/Makefile b/Makefile index e68b94c1..b15183a1 100644 --- a/Makefile +++ b/Makefile @@ -51,3 +51,71 @@ build_wheels_i686: # run tests against all supported python versions tox: @tox + + +lint-rust: + cargo fmt --version + cargo fmt --all -- --check + cargo clippy --version + cargo clippy --tests -- \ + -D warnings \ + -W clippy::pedantic \ + -W clippy::dbg_macro \ + -W clippy::print_stdout \ + -A clippy::cast-possible-truncation \ + -A clippy::cast-possible-wrap \ + -A clippy::cast-precision-loss \ + -A clippy::cast-sign-loss \ + -A clippy::doc-markdown \ + -A clippy::float-cmp \ + -A clippy::fn-params-excessive-bools \ + -A clippy::if-not-else \ + -A clippy::manual-let-else \ + -A clippy::match-bool \ + -A clippy::match-same-arms \ + -A clippy::missing-errors-doc \ + -A clippy::missing-panics-doc \ + -A clippy::module-name-repetitions \ + -A clippy::must-use-candidate \ + -A clippy::needless-pass-by-value \ + -A clippy::similar-names \ + -A clippy::single-match-else \ + -A clippy::struct-excessive-bools \ + -A clippy::too-many-lines \ + -A clippy::unnecessary-wraps \ + -A clippy::unused-self \ + -A clippy::used-underscore-binding + + +format-rust: + cargo fmt --version + cargo fmt --all + cargo clippy --version + cargo clippy --tests --fix --allow-dirty -- \ + -D warnings \ + -W clippy::pedantic \ + -W clippy::dbg_macro \ + -W clippy::print_stdout \ + -A clippy::cast-possible-truncation \ + -A clippy::cast-possible-wrap \ + -A clippy::cast-precision-loss \ + -A clippy::cast-sign-loss \ + -A clippy::doc-markdown \ + -A clippy::float-cmp \ + -A clippy::fn-params-excessive-bools \ + -A clippy::if-not-else \ + -A clippy::manual-let-else \ + -A clippy::match-bool \ + -A clippy::match-same-arms \ + -A clippy::missing-errors-doc \ + -A clippy::missing-panics-doc \ + -A clippy::module-name-repetitions \ + -A clippy::must-use-candidate \ + -A clippy::needless-pass-by-value \ + -A clippy::similar-names \ + -A clippy::single-match-else \ + -A clippy::struct-excessive-bools \ + -A clippy::too-many-lines \ + -A clippy::unnecessary-wraps \ + -A clippy::unused-self \ + -A clippy::used-underscore-binding diff --git a/rust/constants.rs b/rust/constants.rs index e48420b5..3fea9c02 100644 --- a/rust/constants.rs +++ b/rust/constants.rs @@ -3,14 +3,12 @@ pub const EPOCH_YEAR: u32 = 1970; pub const DAYS_PER_N_YEAR: u32 = 365; pub const DAYS_PER_L_YEAR: u32 = 366; -pub const USECS_PER_SEC: u32 = 1000000; - 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 = 146097; +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 @@ -22,9 +20,10 @@ pub const SECS_PER_100_YEARS: [u64; 2] = [ // 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 + 1 * 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. @@ -33,8 +32,6 @@ pub const SECS_PER_YEAR: [u32; 2] = [ DAYS_PER_L_YEAR * SECS_PER_DAY, ]; -pub const MONTHS_PER_YEAR: u32 = 12; - // 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], @@ -55,23 +52,5 @@ pub const MONTHS_OFFSETS: [[i32; 14]; 2] = [ pub const DAY_OF_WEEK_TABLE: [u32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]; -pub const TM_SUNDAY: usize = 0; -pub const TM_MONDAY: usize = 1; -pub const TM_TUESDAY: usize = 2; -pub const TM_WEDNESDAY: usize = 3; -pub const TM_THURSDAY: usize = 4; -pub const TM_FRIDAY: usize = 5; -pub const TM_SATURDAY: usize = 6; - pub const TM_JANUARY: usize = 0; -pub const TM_FEBRUARY: usize = 1; -pub const TM_MARCH: usize = 2; -pub const TM_APRIL: usize = 3; -pub const TM_MAY: usize = 4; -pub const TM_JUNE: usize = 5; -pub const TM_JULY: usize = 6; -pub const TM_AUGUST: usize = 7; -pub const TM_SEPTEMBER: usize = 8; -pub const TM_OCTOBER: usize = 9; -pub const TM_NOVEMBER: usize = 10; pub const TM_DECEMBER: usize = 11; diff --git a/rust/helpers.rs b/rust/helpers.rs index 5edbeb57..364075ac 100644 --- a/rust/helpers.rs +++ b/rust/helpers.rs @@ -5,7 +5,7 @@ use crate::constants::{ }; fn p(year: i32) -> i32 { - return year + year / 4 - year / 100 + year / 400; + year + year / 4 - year / 100 + year / 400 } pub fn is_leap(year: i32) -> bool { @@ -25,7 +25,7 @@ pub fn days_in_year(year: i32) -> u32 { } pub fn week_day(year: i32, month: u32, day: u32) -> u32 { - let y: i32 = year - (month < 3) as i32; + 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; @@ -33,14 +33,14 @@ pub fn week_day(year: i32, month: u32, day: u32) -> u32 { return 7; } - w.abs() as u32 + w.unsigned_abs() } pub fn day_number(year: i32, month: u8, day: u8) -> i32 { - let m = ((month + 9) % 12) as i32; + let m = i32::from((month + 9) % 12); let y = year - m / 10; - return 365 * y + y / 4 - y / 100 + y / 400 + (m * 306 + 5) / 10 + (day as i32 - 1); + 365 * y + y / 4 - y / 100 + y / 400 + (m * 306 + 5) / 10 + (i32::from(day) - 1) } pub fn local_time( @@ -56,7 +56,7 @@ pub fn local_time( seconds -= (10957 * SECS_PER_DAY as usize) as isize; year += 30; // == 2000 } else { - seconds += ((146097 - 10957) * SECS_PER_DAY as usize) as isize; + seconds += ((146_097 - 10957) * SECS_PER_DAY as usize) as isize; year -= 370; // == 1600 } @@ -97,12 +97,12 @@ pub fn local_time( } // Handle months and days - let mut month = (TM_DECEMBER + 1) as usize; + 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) as usize { + while month != (TM_JANUARY + 1) { month_offset = MONTHS_OFFSETS[leap_year][month] as usize; if day > month_offset { day -= month_offset; diff --git a/rust/parsing.rs b/rust/parsing.rs index ce5803ee..c17ed714 100644 --- a/rust/parsing.rs +++ b/rust/parsing.rs @@ -2,24 +2,19 @@ use core::str; use std::{fmt, str::CharIndices}; use crate::{ - constants::{DAYS_PER_MONTHS, MONTHS_OFFSETS}, + constants::MONTHS_OFFSETS, helpers::{days_in_year, is_leap, is_long_year, week_day}, }; -pub struct Duration { - years: i32, -} - #[derive(Debug, Clone)] pub struct ParseError { index: usize, - c: char, message: String, } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} (Position: {})", self.message, self.index.to_string()) + write!(f, "{} (Position: {})", self.message, self.index) } } @@ -40,7 +35,7 @@ pub struct ParsedDateTime { pub time_is_midnight: bool, } -impl<'a> ParsedDateTime { +impl ParsedDateTime { pub fn new() -> ParsedDateTime { ParsedDateTime { year: 0, @@ -72,7 +67,7 @@ pub struct ParsedDuration { pub microseconds: u32, } -impl<'a> ParsedDuration { +impl ParsedDuration { pub fn new() -> ParsedDuration { ParsedDuration { years: 0, @@ -93,7 +88,7 @@ pub struct Parsed { pub second_datetime: Option, } -impl<'a> Parsed { +impl Parsed { pub fn new() -> Parsed { Parsed { datetime: None, @@ -147,8 +142,7 @@ impl<'a> Parser<'a> { fn parse_error(&mut self, message: String) -> ParseError { ParseError { index: self.idx, - c: self.current, - message: message, + message, } } @@ -195,7 +189,7 @@ impl<'a> Parser<'a> { } if self.current >= '0' && self.current <= '9' { - value = 10 * value + self.current.to_digit(10).unwrap() as u32; + value = 10 * value + self.current.to_digit(10).unwrap(); self.inc(); } else { return Err(self.unexpected_character_error(field_name, length - i)); @@ -225,7 +219,7 @@ impl<'a> Parser<'a> { self.parse_time(&mut datetime, false)?; if !self.end() { - return Err(self.parse_error(format!("Unconverted data remains"))); + return Err(self.parse_error("Unconverted data remains".to_string())); } match &parsed.datetime { @@ -255,7 +249,7 @@ impl<'a> Parser<'a> { self.parse_time(&mut datetime, true)?; if !self.end() { - return Err(self.parse_error(format!("Unconverted data remains"))); + return Err(self.parse_error("Unconverted data remains".to_string())); } match &parsed.datetime { @@ -341,19 +335,38 @@ impl<'a> Parser<'a> { datetime.day = 1; } } - } else { - if self.current == 'W' { - // Compact ISO week and day (WwwD) - self.inc(); + } 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; + 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")?; - } + 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) { + 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; + + match self.ordinal_to_ymd(datetime.year, ordinal_day, false) { Ok((year, month, day)) => { datetime.year = year; datetime.month = month; @@ -362,29 +375,8 @@ impl<'a> Parser<'a> { 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 + ordinal_day; - - match self.ordinal_to_ymd(datetime.year, ordinal_day, false) { - Ok((year, month, day)) => { - datetime.year = year; - datetime.month = month; - datetime.day = day; - } - Err(error) => return Err(error), - } - } else { - // Day - datetime.day = ordinal_day as u32 * 10 + self.parse_integer(1, "day")?; - } + // Day + datetime.day = ordinal_day as u32 * 10 + self.parse_integer(1, "day")?; } } @@ -409,7 +401,7 @@ impl<'a> Parser<'a> { return Ok(()); } - return Err(self.parse_error(format!("Unconverted data remains"))); + return Err(self.parse_error("Unconverted data remains".to_string())); } match &parsed.datetime { @@ -512,7 +504,7 @@ impl<'a> Parser<'a> { } if !datetime.extended_date_format { - return Err(self.parse_error(format!("Cannot combine \"basic\" date format with \"extended\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`)."))); + 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 { @@ -563,7 +555,7 @@ impl<'a> Parser<'a> { } if datetime.extended_date_format { - return Err(self.parse_error(format!("Cannot combine \"extended\" date format with \"basic\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`)."))); + 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())); } } } @@ -576,7 +568,7 @@ impl<'a> Parser<'a> { // 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 + datetime.time_is_midnight = true; } if self.current == 'Z' || self.current == '+' || self.current == '-' { @@ -606,24 +598,24 @@ impl<'a> Parser<'a> { tzminute = self.parse_integer(2, "timezone minute")? as i32; } } else { - datetime.tzname = Some("UTC".to_string()) + datetime.tzname = Some("UTC".to_string()); } if tzminute > 59 { - return Err(self.parse_error(format!("timezone minute must be in 0..59"))); + return Err(self.parse_error("timezone minute must be in 0..59".to_string())); } tzminute += tzhour * 60; tzminute *= tzsign; if tzminute.abs() > 1440 { - return Err(self.parse_error(format!("The absolute offset is to large"))); + return Err(self.parse_error("The absolute offset is to large".to_string())); } datetime.offset = Some(tzminute * 60); } - return Ok(()); + Ok(()) } fn parse_duration(&mut self, parsed: &mut Parsed) -> Result<(), ParseError> { @@ -639,7 +631,7 @@ impl<'a> Parser<'a> { 'T' => { if got_t { return Err( - self.parse_error(format!("Repeated time declaration in duration")) + self.parse_error("Repeated time declaration in duration".to_string()) ); } @@ -648,7 +640,7 @@ impl<'a> Parser<'a> { _c => { let (value, op_fraction) = self.parse_duration_number_frac()?; if last_had_fraction { - return Err(self.parse_error(format!("Invalid duration fraction"))); + return Err(self.parse_error("Invalid duration fraction".to_string())); } if op_fraction.is_some() { @@ -663,45 +655,45 @@ impl<'a> Parser<'a> { || duration.microseconds != 0 { return Err( - self.parse_error(format!("Duration units out of order")) + self.parse_error("Duration units out of order".to_string()) ); } duration.hours += value; if let Some(fraction) = op_fraction { - let extra_minutes = fraction * 60 as f64; + 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 = duration.seconds + extra_full_seconds as u32; + duration.seconds += extra_full_seconds as u32; let micro_extra = ((extra_seconds - extra_full_seconds) * 1_000_000.0) .round() as u32; - duration.microseconds = duration.microseconds + micro_extra; + duration.microseconds += micro_extra; } } 'M' => { if duration.seconds != 0 || duration.microseconds != 0 { return Err( - self.parse_error(format!("Duration units out of order")) + self.parse_error("Duration units out of order".to_string()) ); } duration.minutes += value; if let Some(fraction) = op_fraction { - let extra_seconds = fraction * 60 as f64; + let extra_seconds = fraction * 60_f64; let extra_full_seconds = extra_seconds.trunc(); - duration.seconds = duration.seconds + extra_full_seconds as u32; + duration.seconds += extra_full_seconds as u32; let micro_extra = ((extra_seconds - extra_full_seconds) * 1_000_000.0) .round() as u32; - duration.microseconds = duration.microseconds + micro_extra; + duration.microseconds += micro_extra; } } 'S' => { @@ -713,21 +705,24 @@ impl<'a> Parser<'a> { } } _ => { - return Err(self.parse_error(format!("Invalid duration time unit"))) + 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(format!( + 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(format!("Duration units out of order")) + self.parse_error("Duration units out of order".to_string()) ); } @@ -735,14 +730,15 @@ impl<'a> Parser<'a> { } 'M' => { if last_had_fraction { - return Err(self.parse_error(format!( + return Err(self.parse_error( "Fractional months in duration are not supported" - ))); + .to_string(), + )); } if duration.days != 0 { return Err( - self.parse_error(format!("Duration units out of order")) + self.parse_error("Duration units out of order".to_string()) ); } @@ -750,17 +746,17 @@ impl<'a> Parser<'a> { } 'W' => { if duration.years != 0 || duration.months != 0 { - return Err(self.parse_error(format!( - "Basic format durations cannot have weeks" - ))); + 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 as f64; + let extra_days = fraction * 7_f64; let extra_full_days = extra_days.trunc(); - duration.days = duration.days + extra_full_days as u32; + 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; @@ -771,19 +767,19 @@ impl<'a> Parser<'a> { let extra_seconds = ((extra_minutes - extra_full_minutes) * 60.0).round(); let extra_full_seconds = extra_seconds.trunc(); - duration.seconds = duration.seconds + extra_full_seconds as u32; + duration.seconds += extra_full_seconds as u32; let micro_extra = ((extra_seconds - extra_full_seconds) * 1_000_000.0) .round() as u32; - duration.microseconds = duration.microseconds + micro_extra; + duration.microseconds += micro_extra; } } 'D' => { if duration.weeks != 0 { - return Err(self.parse_error(format!( - "Week format durations cannot have days" - ))); + return Err(self.parse_error( + "Week format durations cannot have days".to_string(), + )); } duration.days += value; @@ -798,21 +794,22 @@ impl<'a> Parser<'a> { let extra_seconds = ((extra_minutes - extra_full_minutes) * 60.0).round(); let extra_full_seconds = extra_seconds.trunc(); - duration.seconds = duration.seconds + extra_full_seconds as u32; + duration.seconds += extra_full_seconds as u32; let micro_extra = ((extra_seconds - extra_full_seconds) * 1_000_000.0) .round() as u32; - duration.microseconds = duration.microseconds + micro_extra; + duration.microseconds += micro_extra; } } _ => { - return Err(self.parse_error(format!("Invalid duration time unit"))) + return Err( + self.parse_error("Invalid duration time unit".to_string()) + ) } } } } - _ => break, } self.inc(); @@ -823,7 +820,7 @@ impl<'a> Parser<'a> { parsed.duration = Some(duration); - return Ok(()); + Ok(()) } fn parse_duration_number_frac(&mut self) -> Result<(u32, Option), ParseError> { @@ -837,7 +834,7 @@ impl<'a> Parser<'a> { match self.current { c if c.is_ascii_digit() => { decimal *= 10.0; - decimal += c.to_digit(10).unwrap() as f64; + decimal += f64::from(c.to_digit(10).unwrap()); denominator *= 10.0; } _ => return Ok((value, Some(decimal / denominator))), @@ -850,9 +847,9 @@ impl<'a> Parser<'a> { fn parse_duration_number(&mut self) -> Result { let mut value = match self.current { - c if c.is_ascii_digit() => c.to_digit(10).unwrap() as u32, + c if c.is_ascii_digit() => c.to_digit(10).unwrap(), _ => { - return Err(self.parse_error(format!("Invalid number in duration"))); + return Err(self.parse_error("Invalid number in duration".to_string())); } }; @@ -861,8 +858,8 @@ impl<'a> Parser<'a> { match self.current { c if c.is_ascii_digit() => { - value = value * 10; - value = value + c.to_digit(10).unwrap() as u32; + value *= 10; + value += c.to_digit(10).unwrap(); } _ => return Ok(value), } @@ -878,10 +875,8 @@ impl<'a> Parser<'a> { if iso_week > 53 || iso_week > 52 && !is_long_year(iso_year as i32) { return Err(ParseError { index: self.idx, - c: self.current, message: format!( - "Invalid ISO date: week {} out of range for year {}", - iso_week, iso_year + "Invalid ISO date: week {iso_week} out of range for year {iso_year}" ), }); } @@ -889,8 +884,7 @@ impl<'a> Parser<'a> { if iso_day > 7 { return Err(ParseError { index: self.idx, - c: self.current, - message: format!("Invalid ISO date: week day is invalid"), + message: "Invalid ISO date: week day is invalid".to_string(), }); } @@ -908,35 +902,31 @@ impl<'a> Parser<'a> { ) -> Result<(u32, u32, u32), ParseError> { let mut ord: i32 = ordinal; let mut y: u32 = year; - let mut leap: usize = is_leap(y as i32) as usize; + 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: {} is too small for year {}", - ordinal.to_string(), - year.to_string() + "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 = is_leap(y as i32) as usize; + 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: {} is too large for year {}", - ordinal.to_string(), - year.to_string() + "Invalid ordinal day: {ordinal} is too large for year {year}" ))); } // Next year ord -= days_in_year(y as i32) as i32; y += 1; - leap = is_leap(y as i32) as usize; + leap = usize::from(is_leap(y as i32)); } for i in 1..14 { @@ -944,14 +934,12 @@ impl<'a> Parser<'a> { let day = ord as u32 - MONTHS_OFFSETS[leap][i - 1] as u32; let month = (i - 1) as u32; - return Ok((y as u32, month, day)); + return Ok((y, month, day)); } } Err(self.parse_error(format!( - "Invalid ordinal day: {} is too large for year {}", - ordinal.to_string(), - year.to_string() + "Invalid ordinal day: {ordinal} is too large for year {year}" ))) } } diff --git a/rust/python/helpers.rs b/rust/python/helpers.rs index 6ac33be5..4a53e595 100644 --- a/rust/python/helpers.rs +++ b/rust/python/helpers.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use pyo3::{ intern, prelude::*, @@ -28,7 +30,7 @@ struct DateTimeInfo<'py> { impl PartialEq for DateTimeInfo<'_> { fn eq(&self, other: &Self) -> bool { - return ( + ( self.year, self.month, self.day, @@ -45,13 +47,13 @@ impl PartialEq for DateTimeInfo<'_> { other.minute, other.second, other.microsecond, - )); + )) } } impl PartialOrd for DateTimeInfo<'_> { fn partial_cmp(&self, other: &Self) -> Option { - return ( + ( self.year, self.month, self.day, @@ -68,7 +70,7 @@ impl PartialOrd for DateTimeInfo<'_> { other.minute, other.second, other.microsecond, - )); + )) } } @@ -82,7 +84,7 @@ pub fn get_tz_name<'py>(py: Python, dt: &'py PyAny) -> PyResult<&'py str> { let tzinfo = dt.getattr("tzinfo"); match tzinfo { - Err(_) => return Ok(tz), + Err(_) => Ok(tz), Ok(tzinfo) => { if tzinfo.is_none() { return Ok(tz); @@ -114,14 +116,14 @@ pub fn get_tz_name<'py>(py: Python, dt: &'py PyAny) -> PyResult<&'py str> { .unwrap(); return tzname.to_str(); - } else { - return Ok(tz); } + + Ok(tz) } } } -pub fn get_offset<'py>(dt: &'py PyAny) -> PyResult { +pub fn get_offset(dt: &PyAny) -> PyResult { if !PyDateTime::is_type_of(dt) { return Ok(0); } @@ -134,7 +136,7 @@ pub fn get_offset<'py>(dt: &'py PyAny) -> PyResult { let offset: &PyDelta = tzinfo.call_method1("utcoffset", (dt,))?.downcast()?; - return Ok(offset.get_days() * SECS_PER_DAY as i32 + offset.get_seconds()); + Ok(offset.get_days() * SECS_PER_DAY as i32 + offset.get_seconds()) } #[pyfunction] @@ -171,8 +173,8 @@ pub fn precise_diff<'py>(py: Python, dt1: &'py PyAny, dt2: &'py PyAny) -> PyResu let mut sign = 1; let mut dtinfo1 = DateTimeInfo { year: dt1.downcast::()?.get_year(), - month: dt1.downcast::()?.get_month() as i32, - day: dt1.downcast::()?.get_day() as i32, + month: i32::from(dt1.downcast::()?.get_month()), + day: i32::from(dt1.downcast::()?.get_day()), hour: 0, minute: 0, second: 0, @@ -184,8 +186,8 @@ pub fn precise_diff<'py>(py: Python, dt1: &'py PyAny, dt2: &'py PyAny) -> PyResu }; let mut dtinfo2 = DateTimeInfo { year: dt2.downcast::()?.get_year(), - month: dt2.downcast::()?.get_month() as i32, - day: dt2.downcast::()?.get_day() as i32, + month: i32::from(dt2.downcast::()?.get_month()), + day: i32::from(dt2.downcast::()?.get_day()), hour: 0, minute: 0, second: 0, @@ -195,16 +197,16 @@ pub fn precise_diff<'py>(py: Python, dt1: &'py PyAny, dt2: &'py PyAny) -> PyResu offset: get_offset(dt2)?, is_datetime: PyDateTime::is_type_of(dt2), }; - let in_same_tz: bool = dtinfo1.tz == dtinfo2.tz && dtinfo1.tz.len() > 0; + 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 = dt1dt.get_hour() as i32; - dtinfo1.minute = dt1dt.get_minute() as i32; - dtinfo1.second = dt1dt.get_second() as i32; + 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 { @@ -247,9 +249,9 @@ pub fn precise_diff<'py>(py: Python, dt1: &'py PyAny, dt2: &'py PyAny) -> PyResu if dtinfo2.is_datetime { let dt2dt: &PyDateTime = dt2.downcast()?; - dtinfo2.hour = dt2dt.get_hour() as i32; - dtinfo2.minute = dt2dt.get_minute() as i32; - dtinfo2.second = dt2dt.get_second() as i32; + 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 { @@ -305,7 +307,7 @@ pub fn precise_diff<'py>(py: Python, dt1: &'py PyAny, dt2: &'py PyAny) -> PyResu let mut microsecond_diff = dtinfo2.microsecond - dtinfo1.microsecond; if microsecond_diff < 0 { - microsecond_diff += 1000000; + microsecond_diff += 1_000_000; second_diff -= 1; } @@ -339,26 +341,30 @@ pub fn precise_diff<'py>(py: Python, dt1: &'py PyAny, dt2: &'py PyAny) -> PyResu let leap = helpers::is_leap(year); - let days_in_last_month = DAYS_PER_MONTHS[leap as usize][month as usize]; + let days_in_last_month = DAYS_PER_MONTHS[usize::from(leap)][month as usize]; let days_in_month = - DAYS_PER_MONTHS[helpers::is_leap(dtinfo2.year) as usize][dtinfo2.month as usize]; - - if day_diff < days_in_month - days_in_last_month { - // We don't have a full month, we calculate days - if (days_in_last_month < dtinfo1.day) { - day_diff += dtinfo1.day; - } else { + 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; } - } else if day_diff == days_in_month - days_in_last_month { - // We have exactly a full month - // We remove the days difference - // and add one to the months difference - day_diff = 0; - month_diff += 1; - } else { - // We have a full month - day_diff += days_in_last_month; } month_diff -= 1; @@ -369,7 +375,7 @@ pub fn precise_diff<'py>(py: Python, dt1: &'py PyAny, dt2: &'py PyAny) -> PyResu year_diff -= 1; } - return Ok(PreciseDiff { + Ok(PreciseDiff { years: year_diff * sign, months: month_diff * sign, days: day_diff * sign, @@ -378,5 +384,5 @@ pub fn precise_diff<'py>(py: Python, dt1: &'py PyAny, dt2: &'py PyAny) -> PyResu seconds: second_diff * sign, microseconds: microsecond_diff * sign, total_days: total_days * sign, - }); + }) } diff --git a/rust/python/parsing.rs b/rust/python/parsing.rs index 70fb1501..f12782ed 100644 --- a/rust/python/parsing.rs +++ b/rust/python/parsing.rs @@ -4,7 +4,7 @@ use pyo3::types::PyDate; use pyo3::types::PyDateTime; use pyo3::types::PyTime; -use crate::parsing::{ParseError, Parser}; +use crate::parsing::Parser; use crate::python::types::{Duration, FixedTimezone}; #[pyfunction] @@ -24,7 +24,7 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { datetime.hour as u8, datetime.minute as u8, datetime.second as u8, - datetime.microsecond as u32, + datetime.microsecond, Some( Py::new(py, FixedTimezone::new(offset, datetime.tzname))? .to_object(py) @@ -32,7 +32,7 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { ), )?; - return Ok(dt.to_object(py)); + Ok(dt.to_object(py)) } None => { let dt = PyDateTime::new( @@ -43,11 +43,11 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { datetime.hour as u8, datetime.minute as u8, datetime.second as u8, - datetime.microsecond as u32, + datetime.microsecond, None, )?; - return Ok(dt.to_object(py)); + Ok(dt.to_object(py)) } }, (true, false) => { @@ -58,7 +58,7 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { datetime.day as u8, )?; - return Ok(dt.to_object(py)); + Ok(dt.to_object(py)) } (false, true) => match datetime.offset { Some(offset) => { @@ -67,7 +67,7 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { datetime.hour as u8, datetime.minute as u8, datetime.second as u8, - datetime.microsecond as u32, + datetime.microsecond, Some( Py::new(py, FixedTimezone::new(offset, datetime.tzname))? .to_object(py) @@ -75,7 +75,7 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { ), )?; - return Ok(dt.to_object(py)); + Ok(dt.to_object(py)) } None => { let dt = PyTime::new( @@ -83,35 +83,35 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { datetime.hour as u8, datetime.minute as u8, datetime.second as u8, - datetime.microsecond as u32, + datetime.microsecond, None, )?; - return Ok(dt.to_object(py)); + Ok(dt.to_object(py)) } }, - (_, _) => Err(exceptions::PyValueError::new_err(format!("Parsing error"))), + (_, _) => Err(exceptions::PyValueError::new_err( + "Parsing error".to_string(), + )), }, - (None, Some(duration), None) => { - return 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(format!( - "Not yet implemented" - ))), + (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(format!("{}", error))), + Err(error) => Err(exceptions::PyValueError::new_err(format!("{error}"))), } } diff --git a/rust/python/types/duration.rs b/rust/python/types/duration.rs index fca4fe73..fc18f4eb 100644 --- a/rust/python/types/duration.rs +++ b/rust/python/types/duration.rs @@ -24,6 +24,7 @@ pub struct Duration { 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, diff --git a/rust/python/types/precise_diff.rs b/rust/python/types/precise_diff.rs index c9f67f9c..64ca3a65 100644 --- a/rust/python/types/precise_diff.rs +++ b/rust/python/types/precise_diff.rs @@ -24,6 +24,7 @@ pub struct PreciseDiff { 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, diff --git a/rust/python/types/timezone.rs b/rust/python/types/timezone.rs index c3a5565a..d7d801aa 100644 --- a/rust/python/types/timezone.rs +++ b/rust/python/types/timezone.rs @@ -1,5 +1,5 @@ use pyo3::prelude::*; -use pyo3::types::{PyDateTime, PyDelta, PyDict, PyTzInfo}; +use pyo3::types::{PyDelta, PyDict, PyTzInfo}; #[pyclass(module = "_pendulum", extends = PyTzInfo)] #[derive(Clone)] @@ -42,7 +42,7 @@ impl FixedTimezone { let sign = if self.offset < 0 { "-" } else { "+" }; let minutes = self.offset / 60; let (hour, minute) = (minutes.abs() / 60, minutes.abs() % 60); - format!("{}{:.2}:{:.2}", sign, hour, minute) + format!("{sign}{hour:.2}:{minute:.2}") } } } From 0f785b0e43f87e0735597ff1a6236cdaea11894c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Sat, 5 Aug 2023 21:28:01 +0200 Subject: [PATCH 16/19] Address review comments --- Makefile | 62 ++------------------------------------------ _pendulum.pyi | 1 - build.py | 3 +++ pendulum/_helpers.py | 22 ---------------- pendulum/parser.py | 6 ++--- pyproject.toml | 5 +--- 6 files changed, 9 insertions(+), 90 deletions(-) diff --git a/Makefile b/Makefile index b15183a1..468cd9a1 100644 --- a/Makefile +++ b/Makefile @@ -54,68 +54,10 @@ tox: lint-rust: - cargo fmt --version cargo fmt --all -- --check - cargo clippy --version - cargo clippy --tests -- \ - -D warnings \ - -W clippy::pedantic \ - -W clippy::dbg_macro \ - -W clippy::print_stdout \ - -A clippy::cast-possible-truncation \ - -A clippy::cast-possible-wrap \ - -A clippy::cast-precision-loss \ - -A clippy::cast-sign-loss \ - -A clippy::doc-markdown \ - -A clippy::float-cmp \ - -A clippy::fn-params-excessive-bools \ - -A clippy::if-not-else \ - -A clippy::manual-let-else \ - -A clippy::match-bool \ - -A clippy::match-same-arms \ - -A clippy::missing-errors-doc \ - -A clippy::missing-panics-doc \ - -A clippy::module-name-repetitions \ - -A clippy::must-use-candidate \ - -A clippy::needless-pass-by-value \ - -A clippy::similar-names \ - -A clippy::single-match-else \ - -A clippy::struct-excessive-bools \ - -A clippy::too-many-lines \ - -A clippy::unnecessary-wraps \ - -A clippy::unused-self \ - -A clippy::used-underscore-binding + cargo clippy --tests -- -D warnings format-rust: - cargo fmt --version cargo fmt --all - cargo clippy --version - cargo clippy --tests --fix --allow-dirty -- \ - -D warnings \ - -W clippy::pedantic \ - -W clippy::dbg_macro \ - -W clippy::print_stdout \ - -A clippy::cast-possible-truncation \ - -A clippy::cast-possible-wrap \ - -A clippy::cast-precision-loss \ - -A clippy::cast-sign-loss \ - -A clippy::doc-markdown \ - -A clippy::float-cmp \ - -A clippy::fn-params-excessive-bools \ - -A clippy::if-not-else \ - -A clippy::manual-let-else \ - -A clippy::match-bool \ - -A clippy::match-same-arms \ - -A clippy::missing-errors-doc \ - -A clippy::missing-panics-doc \ - -A clippy::module-name-repetitions \ - -A clippy::must-use-candidate \ - -A clippy::needless-pass-by-value \ - -A clippy::similar-names \ - -A clippy::single-match-else \ - -A clippy::struct-excessive-bools \ - -A clippy::too-many-lines \ - -A clippy::unnecessary-wraps \ - -A clippy::unused-self \ - -A clippy::used-underscore-binding + cargo clippy --tests --fix --allow-dirty -- -D warnings diff --git a/_pendulum.pyi b/_pendulum.pyi index 02520a14..74d7d830 100644 --- a/_pendulum.pyi +++ b/_pendulum.pyi @@ -37,5 +37,4 @@ def local_time( unix_time: int, utc_offset: int, microseconds: int ) -> tuple[int, int, int, int, int, int, int]: ... def precise_diff(d1: datetime | date, d2: datetime | date) -> PreciseDiff: ... -def timestamp(dt: datetime) -> int: ... def week_day(year: int, month: int, day: int) -> int: ... diff --git a/build.py b/build.py index 4ff12214..60d3c4ba 100644 --- a/build.py +++ b/build.py @@ -25,6 +25,9 @@ def _build(): maturin("build", "-r", *cargo_args) + # We won't use the wheel built by maturin directly since + # we want Poetry to build it but we need to retrieve the + # compiled extensions from the maturin wheel. wheel = list(wheels_dir.glob("*.whl"))[0] with zipfile.ZipFile(wheel.as_posix()) as whl: whl.extractall(wheels_dir.as_posix()) diff --git a/pendulum/_helpers.py b/pendulum/_helpers.py index 4dd4c726..1b586d31 100644 --- a/pendulum/_helpers.py +++ b/pendulum/_helpers.py @@ -84,28 +84,6 @@ def days_in_year(year: int) -> int: return DAYS_PER_N_YEAR -def timestamp(dt: datetime.datetime) -> int: - year = dt.year - - result = (year - 1970) * 365 + MONTHS_OFFSETS[0][dt.month] - result += (year - 1968) // 4 - result -= (year - 1900) // 100 - result += (year - 1600) // 400 - - if is_leap(year) and dt.month < 3: - result -= 1 - - result += dt.day - 1 - result *= 24 - result += dt.hour - result *= 60 - result += dt.minute - result *= 60 - result += dt.second - - return result - - def local_time( unix_time: int, utc_offset: int, microseconds: int ) -> tuple[int, int, int, int, int, int, int]: diff --git a/pendulum/parser.py b/pendulum/parser.py index 66e33ea1..2cb5ef95 100644 --- a/pendulum/parser.py +++ b/pendulum/parser.py @@ -18,9 +18,9 @@ from pendulum.time import Time try: - from _pendulum import Duration as CDuration + from _pendulum import Duration as RustDuration except ImportError: - CDuration = None # type: ignore[assignment,misc] + RustDuration = None # type: ignore[assignment,misc] def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration: @@ -113,7 +113,7 @@ def _parse(text: str, **options: t.Any) -> Date | DateTime | Time | Duration | I if isinstance(parsed, Duration): return parsed - if CDuration and isinstance(parsed, CDuration): # type: ignore[truthy-function] + if RustDuration is not None and isinstance(parsed, RustDuration): return pendulum.duration( years=parsed.years, months=parsed.months, diff --git a/pyproject.toml b/pyproject.toml index e0382f98..5fd519a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,9 +17,6 @@ packages = [ include = [ { path = "meson.build", format = "sdist" }, { path = "pendulum/py.typed" }, - # C extensions must be included in the wheel distributions - { path = "pendulum/_extensions/*.so", format = "wheel" }, - { path = "pendulum/_extensions/*.pyd", format = "wheel" }, # Typing stubs { path = "*.pyi"}, # Rust source @@ -224,5 +221,5 @@ omit = [ ] [build-system] -requires = ["poetry-core>=1.1.0a6", "maturin>=1,<2"] +requires = ["poetry-core>=1.6.1", "maturin>=1,<2"] build-backend = "poetry.core.masonry.api" From fdd182a1628e2d239cae1281204c0b1bcdac8505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Sun, 6 Aug 2023 12:19:40 +0200 Subject: [PATCH 17/19] Make Rust code more idiomatic Co-Authored-By: Maneren <49210777+maneren@users.noreply.github.com> --- rust/parsing.rs | 186 +++++++++++++--------------------- rust/python/parsing.rs | 2 +- rust/python/types/timezone.rs | 15 ++- 3 files changed, 81 insertions(+), 122 deletions(-) diff --git a/rust/parsing.rs b/rust/parsing.rs index c17ed714..757a3e3b 100644 --- a/rust/parsing.rs +++ b/rust/parsing.rs @@ -124,18 +124,15 @@ impl<'a> Parser<'a> { /// 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) -> bool { - match self.chars.next() { - Some((i, ch)) => { - self.idx = i; - self.current = ch; - true - } - None => { - self.idx = self.src.len(); - self.current = '\0'; - false - } + 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 } } @@ -156,10 +153,10 @@ impl<'a> Parser<'a> { "Unexpected end of string while parsing {}. Expected {} more character{}.", field_name, expected_character_count, - if expected_character_count != 1 { - "s" - } else { + if expected_character_count == 1 { "" + } else { + "s" } )); } @@ -188,8 +185,8 @@ impl<'a> Parser<'a> { ))); } - if self.current >= '0' && self.current <= '9' { - value = 10 * value + self.current.to_digit(10).unwrap(); + 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)); @@ -297,14 +294,11 @@ impl<'a> Parser<'a> { 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), - } + 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) @@ -322,14 +316,12 @@ impl<'a> Parser<'a> { let ordinal_day = (datetime.month * 10 + self.parse_integer(1, "ordinal day")?) as i32; - match self.ordinal_to_ymd(datetime.year, ordinal_day, false) { - Ok((year, month, day)) => { - datetime.year = year; - datetime.month = month; - datetime.day = day; - } - Err(error) => return Err(error), - } + 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; @@ -366,14 +358,11 @@ impl<'a> Parser<'a> { // Ordinal day ordinal_day += datetime.month as i32 * 10; - match self.ordinal_to_ymd(datetime.year, ordinal_day, false) { - Ok((year, month, day)) => { - datetime.year = year; - datetime.month = month; - datetime.day = day; - } - Err(error) => return Err(error), - } + 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")?; @@ -477,9 +466,8 @@ impl<'a> Parser<'a> { let mut i: u8 = 0; while i < 6 { - if self.current >= '0' && self.current <= '9' { - datetime.microsecond = - datetime.microsecond * 10 + self.current.to_digit(10).unwrap(); + 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)); @@ -492,7 +480,7 @@ impl<'a> Parser<'a> { } // Drop extraneous digits - while self.current >= '0' && self.current <= '9' { + while self.current.is_ascii_digit() { self.inc(); } @@ -527,9 +515,8 @@ impl<'a> Parser<'a> { let mut i: u8 = 0; while i < 6 { - if self.current >= '0' && self.current <= '9' { - datetime.microsecond = - datetime.microsecond * 10 + self.current.to_digit(10).unwrap(); + 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)); @@ -542,7 +529,7 @@ impl<'a> Parser<'a> { } // Drop extraneous digits - while self.current >= '0' && self.current <= '9' { + while self.current.is_ascii_digit() { self.inc(); } @@ -571,47 +558,32 @@ impl<'a> Parser<'a> { datetime.time_is_midnight = true; } - if self.current == 'Z' || self.current == '+' || self.current == '-' { + 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 mut tzsign = 0; - - if self.current == '+' { - tzsign = 1; - } else if self.current == '-' { - tzsign = -1; - } - + let tzsign = if self.current == '+' { 1 } else { -1 }; self.inc(); - - let mut tzhour: i32 = 0; - let mut tzminute: i32 = 0; - - if tzsign != 0 { - // Offset hour - tzhour = self.parse_integer(2, "timezone hour")? as i32; - if self.current == ':' { - // Optional separator - self.inc(); - - tzminute = self.parse_integer(2, "timezone minute")? as i32; - } else if !self.end() { - tzminute = self.parse_integer(2, "timezone minute")? as i32; - } - } else { - datetime.tzname = Some("UTC".to_string()); - } - - if tzminute > 59 { - return Err(self.parse_error("timezone minute must be in 0..59".to_string())); + // 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.abs() > 1440 { - return Err(self.parse_error("The absolute offset is to large".to_string())); + if tzminute > 24 * 60 { + return Err(self.parse_error("Timezone offset is too large".to_string())); } - datetime.offset = Some(tzminute * 60); } @@ -825,45 +797,33 @@ impl<'a> Parser<'a> { fn parse_duration_number_frac(&mut self) -> Result<(u32, Option), ParseError> { let value = self.parse_duration_number()?; - if self.current == '.' || self.current == ',' { + let fraction = matches!(self.current, '.' | ',').then(|| { let mut decimal = 0_f64; let mut denominator = 1_f64; - loop { - self.inc(); - match self.current { - c if c.is_ascii_digit() => { - decimal *= 10.0; - decimal += f64::from(c.to_digit(10).unwrap()); - denominator *= 10.0; - } - _ => return Ok((value, Some(decimal / denominator))), - } + while let Some(digit) = self.inc().and_then(|ch| ch.to_digit(10)) { + decimal *= 10.0; + decimal += f64::from(digit); + denominator *= 10.0; } - } else { - Ok((value, None)) - } + + decimal / denominator + }); + + Ok((value, fraction)) } fn parse_duration_number(&mut self) -> Result { - let mut value = match self.current { - c if c.is_ascii_digit() => c.to_digit(10).unwrap(), - _ => { - return Err(self.parse_error("Invalid number in duration".to_string())); - } + let Some(mut value) = self.current.to_digit(10) else { + return Err(self.parse_error("Invalid number in duration".to_string())); }; - loop { - self.inc(); - - match self.current { - c if c.is_ascii_digit() => { - value *= 10; - value += c.to_digit(10).unwrap(); - } - _ => return Ok(value), - } + while let Some(digit) = self.inc().and_then(|ch| ch.to_digit(10)) { + value *= 10; + value += digit; } + + Ok(value) } fn iso_to_ymd( diff --git a/rust/python/parsing.rs b/rust/python/parsing.rs index f12782ed..48fa64c9 100644 --- a/rust/python/parsing.rs +++ b/rust/python/parsing.rs @@ -112,6 +112,6 @@ pub fn parse_iso8601(py: Python, input: &str) -> PyResult { "Not yet implemented".to_string(), )), }, - Err(error) => Err(exceptions::PyValueError::new_err(format!("{error}"))), + Err(error) => Err(exceptions::PyValueError::new_err(error.to_string())), } } diff --git a/rust/python/types/timezone.rs b/rust/python/types/timezone.rs index d7d801aa..1a8bbade 100644 --- a/rust/python/types/timezone.rs +++ b/rust/python/types/timezone.rs @@ -36,14 +36,13 @@ impl FixedTimezone { } fn __str__(&self) -> String { - match self.name.clone() { - Some(n) => n, - None => { - let sign = if self.offset < 0 { "-" } else { "+" }; - let minutes = self.offset / 60; - let (hour, minute) = (minutes.abs() / 60, minutes.abs() % 60); - format!("{sign}{hour:.2}:{minute:.2}") - } + 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}") } } From c197c0ae7822bf0d9f673e5891ec96edf34b9fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Sun, 6 Aug 2023 12:29:13 +0200 Subject: [PATCH 18/19] Fix tests and linting following rebase --- tests/localization/test_tr.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/localization/test_tr.py b/tests/localization/test_tr.py index 5f77abc9..258a5de9 100644 --- a/tests/localization/test_tr.py +++ b/tests/localization/test_tr.py @@ -7,7 +7,7 @@ def test_diff_for_humans(): - with pendulum.test(pendulum.datetime(2016, 8, 29)): + with pendulum.travel_to(pendulum.datetime(2016, 8, 29), freeze=True): diff_for_humans() @@ -49,10 +49,10 @@ def diff_for_humans(): assert d.diff_for_humans(locale=locale) == "2 ay önce" d = pendulum.now().subtract(years=1) - assert d.diff_for_humans(locale=locale) == "1 yıl önce" + assert d.diff_for_humans(locale=locale) == "1 yıl önce" # noqa: RUF001 d = pendulum.now().subtract(years=2) - assert d.diff_for_humans(locale=locale) == "2 yıl önce" + assert d.diff_for_humans(locale=locale) == "2 yıl önce" # noqa: RUF001 d = pendulum.now().add(seconds=1) assert d.diff_for_humans(locale=locale) == "1 saniye sonra" From d29ce1b2d1fc0881df2f8866db0d12a0483eca25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Tue, 8 Aug 2023 22:17:29 +0200 Subject: [PATCH 19/19] Address review comments --- build.py | 4 +++- poetry.lock | 46 ++-------------------------------------------- pyproject.toml | 8 -------- 3 files changed, 5 insertions(+), 53 deletions(-) diff --git a/build.py b/build.py index 60d3c4ba..60c880c4 100644 --- a/build.py +++ b/build.py @@ -35,6 +35,8 @@ def _build(): for extension in wheels_dir.rglob("**/*.so"): shutil.copyfile(extension, Path(__file__).parent.joinpath(extension.name)) + shutil.rmtree(wheels_dir) + def build(setup_kwargs): """ @@ -44,7 +46,7 @@ def build(setup_kwargs): _build() except Exception as e: print( - " Unable to build C extensions, " + " Unable to build Rust extensions, " "Pendulum will use the pure python version of the extensions." ) print(e) diff --git a/poetry.lock b/poetry.lock index 6f6e448f..fa53a303 100644 --- a/poetry.lock +++ b/poetry.lock @@ -421,22 +421,6 @@ files = [ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] -[[package]] -name = "meson" -version = "0.63.2" -description = "A high performance build system" -optional = false -python-versions = ">=3.7" -files = [ - {file = "meson-0.63.2-py3-none-any.whl", hash = "sha256:64a83ef257b2962b52c8b07ad9ec536c2de1b72fd9f14bcd9c21fe45730edd46"}, - {file = "meson-0.63.2.tar.gz", hash = "sha256:16222f17ef76be0542c91c07994f9676ae879f46fc21c0c786a21ef2cb518bbf"}, -] - -[package.extras] -ninja = ["ninja (>=1.8.2)"] -progress = ["tqdm"] -typing = ["mypy", "typing-extensions"] - [[package]] name = "mkdocs" version = "1.3.0" @@ -521,32 +505,6 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] -[[package]] -name = "ninja" -version = "1.10.2.3" -description = "Ninja is a small build system with a focus on speed" -optional = false -python-versions = "*" -files = [ - {file = "ninja-1.10.2.3-py2.py3-none-macosx_10_9_universal2.macosx_10_9_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:d5e0275d28997a750a4f445c00bdd357b35cc334c13cdff13edf30e544704fbd"}, - {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ea785bf6a15727040835256577239fa3cf5da0d60e618c307aa5efc31a1f0ce"}, - {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29570a18d697fc84d361e7e6330f0021f34603ae0fcb0ef67ae781e9814aae8d"}, - {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a1d84d4c7df5881bfd86c25cce4cf7af44ba2b8b255c57bc1c434ec30a2dfc"}, - {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9ca8dbece144366d5f575ffc657af03eb11c58251268405bc8519d11cf42f113"}, - {file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:279836285975e3519392c93c26e75755e8a8a7fafec9f4ecbb0293119ee0f9c6"}, - {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cc8b31b5509a2129e4d12a35fc21238c157038022560aaf22e49ef0a77039086"}, - {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:688167841b088b6802e006f911d911ffa925e078c73e8ef2f88286107d3204f8"}, - {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:840a0b042d43a8552c4004966e18271ec726e5996578f28345d9ce78e225b67e"}, - {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:84be6f9ec49f635dc40d4b871319a49fa49b8d55f1d9eae7cd50d8e57ddf7a85"}, - {file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6bd76a025f26b9ae507cf8b2b01bb25bb0031df54ed685d85fc559c411c86cf4"}, - {file = "ninja-1.10.2.3-py2.py3-none-win32.whl", hash = "sha256:740d61fefb4ca13573704ee8fe89b973d40b8dc2a51aaa4e9e68367233743bb6"}, - {file = "ninja-1.10.2.3-py2.py3-none-win_amd64.whl", hash = "sha256:0560eea57199e41e86ac2c1af0108b63ae77c3ca4d05a9425a750e908135935a"}, - {file = "ninja-1.10.2.3.tar.gz", hash = "sha256:e1b86ad50d4e681a7dbdff05fc23bb52cb773edb90bc428efba33fa027738408"}, -] - -[package.extras] -test = ["codecov (>=2.0.5)", "coverage (>=4.2)", "flake8 (>=3.0.4)", "pytest (>=4.5.0)", "pytest-cov (>=2.7.1)", "pytest-runner (>=5.1)", "pytest-virtualenv (>=1.7.0)", "virtualenv (>=15.0.3)"] - [[package]] name = "nodeenv" version = "1.7.0" @@ -655,7 +613,7 @@ name = "pycparser" version = "2.21" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "*" files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, @@ -1234,4 +1192,4 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "7a4cee3a29551de99a31bcf181c7a90cdf9c2979fb3cfbb3a3981d6a0b1c7488" +content-hash = "658e22a0828244e01d1639cbacfbaa84a6ad345aade89e5e28722fb39401db37" diff --git a/pyproject.toml b/pyproject.toml index 5fd519a8..4f64581d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ packages = [ { include = "tests", format = "sdist" }, ] include = [ - { path = "meson.build", format = "sdist" }, { path = "pendulum/py.typed" }, # Typing stubs { path = "*.pyi"}, @@ -60,13 +59,6 @@ babel = "^2.10.3" cleo = ">=1,<3" tox = "^3.25.1" -[tool.poetry.group.build] -optional = true - -[tool.poetry.group.build.dependencies] -meson = "^0.63.2" -ninja = "^1.10.2.3" - [tool.poetry.group.benchmark.dependencies] pytest-codspeed = "^1.2.2"