From 178ca3c066a1ef920b112da741625099b6dc07da Mon Sep 17 00:00:00 2001 From: gennyble Date: Sun, 20 Sep 2026 19:43:42 -0500 Subject: move timeparse to it's own crate --- Cargo.lock | 8 ++ Cargo.toml | 3 +- awake/Cargo.toml | 8 +- awake/src/main.rs | 13 ++- awake/src/timeparse.rs | 218 ------------------------------------------------ awake/src/util.rs | 14 ++-- timeparse/Cargo.toml | 11 +++ timeparse/src/format.rs | 25 ++++++ timeparse/src/lib.rs | 211 ++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 270 insertions(+), 241 deletions(-) delete mode 100755 awake/src/timeparse.rs create mode 100644 timeparse/Cargo.toml create mode 100644 timeparse/src/format.rs create mode 100755 timeparse/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 23ee9c9..1813ea6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,7 @@ dependencies = [ "sha2", "snafu", "time", + "timeparse", "tokio", "tokio-util", "tracing", @@ -1491,6 +1492,13 @@ dependencies = [ "time-core", ] +[[package]] +name = "timeparse" +version = "1.0.0" +dependencies = [ + "time", +] + [[package]] name = "tinystr" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index ecbe589..73829c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["awake", "markup", "pushwhatever", "shovel", "wikarden"] +members = ["awake", "markup", "pushwhatever", "shovel", "timeparse", "wikarden"] [workspace.dependencies] bempline = { git = "https://github.com/gennyble/bempline" } @@ -13,6 +13,7 @@ time = { version = "0.3.55", features = ["local-offset"] } # Local Crates markup = { path = "markup" } wikarden = { path = "wikarden" } +timeparse = { path = "timeparse" } [workspace.dependencies.scurvy] git = "https://git.dreamy.place/crates/scurvy/" diff --git a/awake/Cargo.toml b/awake/Cargo.toml index f48f315..748e0b3 100755 --- a/awake/Cargo.toml +++ b/awake/Cargo.toml @@ -18,12 +18,8 @@ confindent = { git = "https://github.com/gennyble/confindent", branch = "v3.0-wr cutie = { git = "https://git.dreamy.place/crates/cutie" } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -time = { workspace = true, features = [ - "macros", - "parsing", - "formatting", - "local-offset", -] } +timeparse.workspace = true +time.workspace = true sha2 = "0.10.8" url = "2.5.0" rusqlite = { version = "0.40.2", features = ["bundled", "time", "fallible_uint"] } diff --git a/awake/src/main.rs b/awake/src/main.rs index 7a97468..24e9692 100755 --- a/awake/src/main.rs +++ b/awake/src/main.rs @@ -8,7 +8,6 @@ mod me; mod query; mod settings; mod templated; -mod timeparse; mod util; mod wiki; @@ -41,7 +40,7 @@ use settings::Settings; use templated::Frontmatter; use tokio_util::io::ReaderStream; use tracing_subscriber::{prelude::*, EnvFilter}; -use util::{Referer, SessionId}; +use util::{Referer, SessionHash}; use wikarden::Wikarden; use crate::{ @@ -150,7 +149,7 @@ async fn index_handler( state: State, fse: Extension, se: Extension, - sid: SessionId, + sid: SessionHash, rfr: Option, ) -> Result { handler(state, fse, se, sid, rfr, Path(String::from("/"))).await @@ -161,7 +160,7 @@ async fn handler( State(state): State, Extension(fs): Extension, Extension(settings): Extension, - sid: SessionId, + sid: SessionHash, rfr: Option, Path(path): Path, ) -> Result { @@ -276,7 +275,7 @@ async fn send_template( webpath: Webpath, state: AwakeState, settings: Settings, - sid: SessionId, + sid: SessionHash, ) -> Result { tracing::trace!("[{sid}] sending template"); let template_stem = templated.frontmatter.get("template").expect("no template"); @@ -395,8 +394,8 @@ async fn send_template( break 'published; } Some(Ok(datetime)) => { - let published_human = timeparse::format_long(&datetime); - let published_machine = timeparse::iso8601(&datetime); + let published_human = timeparse::format::human_long(&datetime); + let published_machine = timeparse::format::iso8601(&datetime); variables!(published_pattern, published_human, published_machine); template.set_pattern(published_pattern); diff --git a/awake/src/timeparse.rs b/awake/src/timeparse.rs deleted file mode 100755 index 05e0226..0000000 --- a/awake/src/timeparse.rs +++ /dev/null @@ -1,218 +0,0 @@ -use time::{ - error::{Parse, TryFromParsed}, - format_description::{well_known, FormatItem}, - macros::{format_description, offset, time}, - Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday, -}; - -#[allow(unused)] -const FMT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day] [hour]:[minute]"); - -const DATE: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); -const TIME_24HOUR: &[FormatItem<'_>] = format_description!("[hour padding:zero]:[minute]"); -const TIME_12HOUR: &[FormatItem<'_>] = - format_description!("[hour padding:none]:[minute][period case:lower case_sensitive:false]"); - -#[allow(unused)] -const OFFSET: &[FormatItem<'_>] = - format_description!("[offset_hour sign:mandatory]:[offset_minute]"); - -const CST: UtcOffset = offset!(-06:00); -const CDT: UtcOffset = offset!(-05:00); - -// Ripped from infica with the month of Sol removed. I just want the 3 letter months -/// Capitalized months (long, short) and lowercase months (long, short). -// it seems useful to have the lowercase here so we don't have to always call -// to_lowercase -const MONTHS: [[&str; 4]; 12] = [ - ["January", "Jan", "january", "jan"], - ["February", "Feb", "february", "feb"], - ["March", "Mar", "march", "mar"], - ["April", "Apr", "april", "apr"], - ["May", "May", "may", "may"], - ["June", "Jun", "june", "jun"], - ["July", "Jul", "july", "jul"], - ["August", "Aug", "august", "aug"], - ["September", "Sep", "september", "sep"], - ["October", "Oct", "october", "oct"], - ["November", "Nov", "november", "nov"], - ["December", "Dec", "december", "dec"], -]; - -/// Offset for the united states in Central Time. Accounts for DST -/// DST starts the 2nd sunday in March and ends the 1st sunday in November. -/// https://www.nist.gov/pml/time-and-frequency-division/popular-links/daylight-saving-time-dst -fn us_dst_central_offset(datetime: PrimitiveDateTime) -> UtcOffset { - let second_sunday_march = { - let mut seen_sunday = false; - let mut curr = Date::from_calendar_date(datetime.year(), time::Month::March, 1).unwrap(); - - loop { - if curr.weekday() == Weekday::Sunday { - if seen_sunday { - break PrimitiveDateTime::new(curr, time!(02:00 AM)); - } else { - seen_sunday = true; - } - } - - curr = curr.next_day().unwrap(); - } - }; - - let first_sunday_november = { - let mut curr = Date::from_calendar_date(datetime.year(), time::Month::November, 1).unwrap(); - - loop { - if curr.weekday() == Weekday::Sunday { - break PrimitiveDateTime::new(curr, time!(02:00 AM)); - } else { - curr = curr.next_day().unwrap(); - } - } - }; - - if datetime >= second_sunday_march && datetime < first_sunday_november { - CDT - } else { - CST - } -} - -fn parse_offset(raw: &str) -> UtcOffset { - match raw.to_ascii_uppercase().as_str() { - "CST" => CST, - "CDT" => CDT, - "UTC" => UtcOffset::UTC, - _ => unimplemented!(), - } -} - -fn parse_time(raw: &str) -> Result { - match Time::parse(raw, TIME_24HOUR) { - Err(_e) => Time::parse(raw, TIME_12HOUR), - Ok(t) => Ok(t), - } -} - -fn parse_date(raw: &str) -> Result { - Date::parse(raw, DATE) -} - -/// Parses an OffsetDateTime from any of the following formats: -/// - date only: 2024-04-13 -/// - date and time: 2024-04-13 2:56 -/// - dat, time, and offset: 2024-04-13 2:56 CDT -/// -/// If a time is not procided, noon (12:00) is assumed. -/// -/// If an offset is not provided it will be assumed to be US Central Time and -/// it will correct for daylight savings. -pub fn parse(raw: &str) -> Result { - let mut splits = raw.trim().split(' '); - - let date = match splits.next() { - None => return Err(Parse::TryFromParsed(TryFromParsed::InsufficientInformation)), - Some(raw) => parse_date(raw)?, - }; - - let time = match splits.next() { - None => time!(12:00:00), - Some(raw) => parse_time(raw)?, - }; - - let calculated_offset = us_dst_central_offset(PrimitiveDateTime::new(date, time)); - let offset = match splits.next() { - None => calculated_offset, - Some(raw) => { - let offset = parse_offset(raw); - if offset != calculated_offset { - //FIXME: gen 2024-12; warn here but format it rightly. - () - } - - offset - } - }; - - Ok(OffsetDateTime::new_in_offset(date, time, offset)) -} - -pub fn format_long(datetime: &OffsetDateTime) -> String { - let year = datetime.year(); - let month = MONTHS[datetime.month() as usize - 1][1]; - let day = datetime.day(); - let weekday = datetime.weekday(); - - format!("{weekday} {day}, {month} {year}") -} - -pub fn iso8601(datetime: &OffsetDateTime) -> String { - datetime - .format(&well_known::Iso8601::DATE_TIME_OFFSET) - .unwrap() -} - -#[cfg(test)] -mod test { - use time::{ - macros::{datetime, offset}, - OffsetDateTime, PrimitiveDateTime, - }; - - use crate::timeparse::{parse_offset, us_dst_central_offset, CDT, CST, FMT}; - - #[test] - fn calculates_cst_cdt_correctly() { - fn daylight(raw: &str) { - let date = PrimitiveDateTime::parse(raw, FMT).unwrap(); - - if us_dst_central_offset(date) != CDT { - panic!("CDT failed on {raw}") - } - } - - fn standard(raw: &str) { - let date = PrimitiveDateTime::parse(raw, FMT).unwrap(); - - if us_dst_central_offset(date) != CST { - panic!("CST failed on {raw}") - } - } - - // make sure CST and CDT are what we expect - assert_eq!(CST, offset!(-06:00)); - assert_eq!(CDT, offset!(-05:00)); - - daylight("2024-04-13 02:56"); - daylight("2024-03-10 14:00"); - daylight("2024-03-10 02:00"); - - standard("2024-12-01 00:00"); - standard("2024-11-03 14:00"); - standard("2024-11-03 02:00"); - - // other years - daylight("2023-03-12 12:00"); // DST start Mar 12, end Nov 5 - standard("2023-03-11 23:00"); - - standard("2023-11-05 12:00"); - daylight("2023-11-04 23:00"); - } - - #[test] - fn paress_timezone_code() { - assert_eq!(parse_offset("CST"), CST); - assert_eq!(parse_offset("CDT"), CDT); - } - - fn test_parse(raw: &str, expected: OffsetDateTime) { - assert_eq!(super::parse(raw).unwrap(), expected) - } - - #[test] - fn parse_date_only() { - test_parse("2024-04-13", datetime!(2024-04-13 12:00 -05:00)); - test_parse("2024-01-01", datetime!(2024-01-01 12:00 -06:00)); - } -} diff --git a/awake/src/util.rs b/awake/src/util.rs index 90cbadc..c7ea61d 100755 --- a/awake/src/util.rs +++ b/awake/src/util.rs @@ -7,7 +7,6 @@ use axum::{ RequestPartsExt, }; use sha2::{Digest, Sha256}; -use time::{format_description::FormatItem, macros::format_description, OffsetDateTime}; #[derive(Debug, Clone)] pub struct RemoteIp(String); @@ -41,12 +40,10 @@ impl Deref for RemoteIp { } } -const FMT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); - #[derive(Debug, Copy, Clone)] -pub struct SessionId([u8; 4]); +pub struct SessionHash([u8; 4]); -impl FromRequestParts for SessionId +impl FromRequestParts for SessionHash where S: Send + Sync, { @@ -65,9 +62,8 @@ where .and_then(|ua| ua.to_str().ok()) .unwrap_or("unknown"); - let date = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); - - let string = format!("{remote} - {agent} - {}", date.format(FMT).unwrap()); + let date = timeparse::whichever_now(); + let string = timeparse::format::iso_date(&date.date()); let mut hasher = Sha256::new(); hasher.update(string.as_bytes()); @@ -78,7 +74,7 @@ where } } -impl fmt::Display for SessionId { +impl fmt::Display for SessionHash { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for byte in self.0.as_slice() { write!(f, "{byte:02X}")?; diff --git a/timeparse/Cargo.toml b/timeparse/Cargo.toml new file mode 100644 index 0000000..d011d9f --- /dev/null +++ b/timeparse/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "timeparse" +version = "1.0.0" +edition = "2024" + +[dependencies] +time = { workspace = true, features = [ + "macros", + "parsing", + "local-offset", +] } diff --git a/timeparse/src/format.rs b/timeparse/src/format.rs new file mode 100644 index 0000000..50fa117 --- /dev/null +++ b/timeparse/src/format.rs @@ -0,0 +1,25 @@ +use time::{Date, OffsetDateTime, format_description::well_known}; + +use crate::MONTHS; + +pub fn human_long(datetime: &OffsetDateTime) -> String { + let year = datetime.year(); + let month = MONTHS[datetime.month() as usize - 1][1]; + let day = datetime.day(); + let weekday = datetime.weekday(); + + format!("{weekday} {day}, {month} {year}") +} + +pub fn iso8601(datetime: &OffsetDateTime) -> String { + datetime + .format(&well_known::Iso8601::DATE_TIME_OFFSET) + .unwrap() +} + +pub fn iso_date(date: &Date) -> String { + let year = date.year(); + let month = date.month(); + let day = date.day(); + format!("{year}-{month:02}-{day:02}") +} diff --git a/timeparse/src/lib.rs b/timeparse/src/lib.rs new file mode 100755 index 0000000..3c8da8e --- /dev/null +++ b/timeparse/src/lib.rs @@ -0,0 +1,211 @@ +use time::{ + Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday, + error::{Parse, TryFromParsed}, + format_description::FormatItem, + macros::{format_description, offset, time}, +}; + +pub mod format; + +#[allow(unused)] +pub(crate) const FMT: &[FormatItem<'_>] = + format_description!("[year]-[month]-[day] [hour]:[minute]"); + +pub(crate) const DATE: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); +pub(crate) const TIME_24HOUR: &[FormatItem<'_>] = + format_description!("[hour padding:zero]:[minute]"); +pub(crate) const TIME_12HOUR: &[FormatItem<'_>] = + format_description!("[hour padding:none]:[minute][period case:lower case_sensitive:false]"); + +#[allow(unused)] +pub(crate) const OFFSET: &[FormatItem<'_>] = + format_description!("[offset_hour sign:mandatory]:[offset_minute]"); + +pub(crate) const CST: UtcOffset = offset!(-06:00); +pub(crate) const CDT: UtcOffset = offset!(-05:00); + +// Ripped from infica with the month of Sol removed. I just want the 3 letter months +/// Capitalized months (long, short) and lowercase months (long, short). +// it seems useful to have the lowercase here so we don't have to always call +// to_lowercase +pub(crate) const MONTHS: [[&str; 4]; 12] = [ + ["January", "Jan", "january", "jan"], + ["February", "Feb", "february", "feb"], + ["March", "Mar", "march", "mar"], + ["April", "Apr", "april", "apr"], + ["May", "May", "may", "may"], + ["June", "Jun", "june", "jun"], + ["July", "Jul", "july", "jul"], + ["August", "Aug", "august", "aug"], + ["September", "Sep", "september", "sep"], + ["October", "Oct", "october", "oct"], + ["November", "Nov", "november", "nov"], + ["December", "Dec", "december", "dec"], +]; + +/// Offset for the united states in Central Time. Accounts for DST +/// DST starts the 2nd sunday in March and ends the 1st sunday in November. +/// https://www.nist.gov/pml/time-and-frequency-division/popular-links/daylight-saving-time-dst +fn us_dst_central_offset(datetime: PrimitiveDateTime) -> UtcOffset { + let second_sunday_march = { + let mut seen_sunday = false; + let mut curr = Date::from_calendar_date(datetime.year(), time::Month::March, 1).unwrap(); + + loop { + if curr.weekday() == Weekday::Sunday { + if seen_sunday { + break PrimitiveDateTime::new(curr, time!(02:00 AM)); + } else { + seen_sunday = true; + } + } + + curr = curr.next_day().unwrap(); + } + }; + + let first_sunday_november = { + let mut curr = Date::from_calendar_date(datetime.year(), time::Month::November, 1).unwrap(); + + loop { + if curr.weekday() == Weekday::Sunday { + break PrimitiveDateTime::new(curr, time!(02:00 AM)); + } else { + curr = curr.next_day().unwrap(); + } + } + }; + + if datetime >= second_sunday_march && datetime < first_sunday_november { + CDT + } else { + CST + } +} + +fn parse_offset(raw: &str) -> UtcOffset { + match raw.to_ascii_uppercase().as_str() { + "CST" => CST, + "CDT" => CDT, + "UTC" => UtcOffset::UTC, + _ => unimplemented!(), + } +} + +fn parse_time(raw: &str) -> Result { + match Time::parse(raw, TIME_24HOUR) { + Err(_e) => Time::parse(raw, TIME_12HOUR), + Ok(t) => Ok(t), + } +} + +fn parse_date(raw: &str) -> Result { + Date::parse(raw, DATE) +} + +/// Parses an OffsetDateTime from any of the following formats: +/// - date only: 2024-04-13 +/// - date and time: 2024-04-13 2:56 +/// - dat, time, and offset: 2024-04-13 2:56 CDT +/// +/// If a time is not procided, noon (12:00) is assumed. +/// +/// If an offset is not provided it will be assumed to be US Central Time and +/// it will correct for daylight savings. +pub fn parse(raw: &str) -> Result { + let mut splits = raw.trim().split(' '); + + let date = match splits.next() { + None => return Err(Parse::TryFromParsed(TryFromParsed::InsufficientInformation)), + Some(raw) => parse_date(raw)?, + }; + + let time = match splits.next() { + None => time!(12:00:00), + Some(raw) => parse_time(raw)?, + }; + + let calculated_offset = us_dst_central_offset(PrimitiveDateTime::new(date, time)); + let offset = match splits.next() { + None => calculated_offset, + Some(raw) => { + let offset = parse_offset(raw); + if offset != calculated_offset { + //FIXME: gen 2024-12; warn here but format it rightly. + () + } + + offset + } + }; + + Ok(OffsetDateTime::new_in_offset(date, time, offset)) +} + +pub fn whichever_now() -> OffsetDateTime { + OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()) +} + +#[cfg(test)] +mod test { + use time::{ + OffsetDateTime, PrimitiveDateTime, + macros::{datetime, offset}, + }; + + use crate::{CDT, CST, FMT, parse_offset, us_dst_central_offset}; + + #[test] + fn calculates_cst_cdt_correctly() { + fn daylight(raw: &str) { + let date = PrimitiveDateTime::parse(raw, FMT).unwrap(); + + if us_dst_central_offset(date) != CDT { + panic!("CDT failed on {raw}") + } + } + + fn standard(raw: &str) { + let date = PrimitiveDateTime::parse(raw, FMT).unwrap(); + + if us_dst_central_offset(date) != CST { + panic!("CST failed on {raw}") + } + } + + // make sure CST and CDT are what we expect + assert_eq!(CST, offset!(-06:00)); + assert_eq!(CDT, offset!(-05:00)); + + daylight("2024-04-13 02:56"); + daylight("2024-03-10 14:00"); + daylight("2024-03-10 02:00"); + + standard("2024-12-01 00:00"); + standard("2024-11-03 14:00"); + standard("2024-11-03 02:00"); + + // other years + daylight("2023-03-12 12:00"); // DST start Mar 12, end Nov 5 + standard("2023-03-11 23:00"); + + standard("2023-11-05 12:00"); + daylight("2023-11-04 23:00"); + } + + #[test] + fn paress_timezone_code() { + assert_eq!(parse_offset("CST"), CST); + assert_eq!(parse_offset("CDT"), CDT); + } + + fn test_parse(raw: &str, expected: OffsetDateTime) { + assert_eq!(super::parse(raw).unwrap(), expected) + } + + #[test] + fn parse_date_only() { + test_parse("2024-04-13", datetime!(2024-04-13 12:00 -05:00)); + test_parse("2024-01-01", datetime!(2024-01-01 12:00 -06:00)); + } +} -- cgit 1.4.1-3-g733a5