diff --git a/Cargo.lock b/Cargo.lock index cc648d9..365e46e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1620,7 +1620,6 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" name = "dx-admin-template" version = "0.1.0" dependencies = [ - "chrono", "dioxus", "js-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 6ea72ff..caf629a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ edition = "2021" [dependencies] dioxus = { version = "0.7.1", features = ["router", "fullstack"] } -chrono = { version = "0.4", default-features = false, features = ["std"] } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3" diff --git a/src/components/ui/USAGE.md b/src/components/ui/USAGE.md index bd10d27..1da978c 100644 --- a/src/components/ui/USAGE.md +++ b/src/components/ui/USAGE.md @@ -966,8 +966,7 @@ fn InteractiveTableSample() -> Element { - `on_select` 返回选中的 `NaiveDate`。 ```rust -use crate::components::ui::Calendar; -use chrono::NaiveDate; +use crate::{components::ui::Calendar, time::NaiveDate}; use dioxus::prelude::*; #[component] @@ -992,8 +991,10 @@ fn CalendarSample() -> Element { - `on_change` 返回新的区间或 `None`。 ```rust -use crate::components::ui::{DateRange, DateRangePicker}; -use chrono::NaiveDate; +use crate::{ + components::ui::{DateRange, DateRangePicker}, + time::NaiveDate, +}; use dioxus::prelude::*; #[component] diff --git a/src/components/ui/calendar.rs b/src/components/ui/calendar.rs index 7454450..ae24ee6 100644 --- a/src/components/ui/calendar.rs +++ b/src/components/ui/calendar.rs @@ -1,4 +1,4 @@ -use chrono::{Datelike, Duration, NaiveDate}; +use crate::time::{Duration, NaiveDate}; use dioxus::prelude::*; use super::utils::merge_class; const WEEKDAY_LABELS: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; diff --git a/src/components/ui/date_range_picker.rs b/src/components/ui/date_range_picker.rs index 48770f5..046b027 100644 --- a/src/components/ui/date_range_picker.rs +++ b/src/components/ui/date_range_picker.rs @@ -1,9 +1,12 @@ use super::button::{Button, ButtonSize, ButtonVariant}; -use chrono::{Datelike, Duration, NaiveDate}; +use super::utils::merge_class; +use crate::{ + components::ui::PopoverHandle, + time::{Duration, NaiveDate}, +}; use dioxus::prelude::*; -use super::utils::merge_class;#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(target_arch = "wasm32"))] use std::time::SystemTime; -use crate::components::ui::PopoverHandle; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct DateRange { @@ -77,9 +80,9 @@ fn today_date() -> NaiveDate { #[cfg(not(target_arch = "wasm32"))] fn today_date() -> NaiveDate { - let now = SystemTime::now(); - let datetime: chrono::DateTime = now.into(); - datetime.date_naive() + NaiveDate::from_system_time(SystemTime::now()) + .or_else(|| NaiveDate::from_ymd_opt(1970, 1, 1)) + .expect("system time within supported range") } fn describe_range(range: DateRange) -> String { diff --git a/src/main.rs b/src/main.rs index cab0ac1..67b2b4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ // need dioxus use dioxus::prelude::*; +mod time; use views::{Components, Home, Navbar, Orders}; /// Define a components module that contains all shared components for our app. diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..9f1a497 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,297 @@ +use std::fmt::{self, Display, Formatter, Write}; +use std::ops::{Add, AddAssign, Sub, SubAssign}; +use std::time::{SystemTime, UNIX_EPOCH}; + +const SECONDS_PER_DAY: i64 = 86_400; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NaiveDate { + days_since_epoch: i32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Duration { + days: i64, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Weekday { + Monday = 0, + Tuesday = 1, + Wednesday = 2, + Thursday = 3, + Friday = 4, + Saturday = 5, + Sunday = 6, +} + +impl Weekday { + pub fn num_days_from_monday(self) -> u8 { + self as u8 + } +} + +impl Duration { + pub fn days(days: i64) -> Self { + Self { days } + } + + pub fn num_days(self) -> i64 { + self.days + } +} + +#[derive(Clone, Copy)] +pub struct FormattedDate<'a> { + date: NaiveDate, + pattern: &'a str, +} + +impl<'a> Display for FormattedDate<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(&self.date.format_internal(self.pattern)) + } +} + +impl NaiveDate { + pub fn from_ymd_opt(year: i32, month: u32, day: u32) -> Option { + if month == 0 || month > 12 { + return None; + } + if day == 0 || day > days_in_month(year, month) { + return None; + } + Some(Self { + days_since_epoch: days_from_civil(year, month, day)?, + }) + } + + pub fn with_day(self, day: u32) -> Option { + Self::from_ymd_opt(self.year(), self.month(), day) + } + + pub fn year(self) -> i32 { + self.components().0 + } + + pub fn month(self) -> u32 { + self.components().1 + } + + pub fn day(self) -> u32 { + self.components().2 + } + + pub fn weekday(self) -> Weekday { + let idx = (self.days_since_epoch as i64 + 3).rem_euclid(7) as u8; + match idx { + 0 => Weekday::Monday, + 1 => Weekday::Tuesday, + 2 => Weekday::Wednesday, + 3 => Weekday::Thursday, + 4 => Weekday::Friday, + 5 => Weekday::Saturday, + _ => Weekday::Sunday, + } + } + + pub fn format<'a>(self, pattern: &'a str) -> FormattedDate<'a> { + FormattedDate { date: self, pattern } + } + + pub fn from_system_time(time: SystemTime) -> Option { + match time.duration_since(UNIX_EPOCH) { + Ok(duration) => { + let days = (duration.as_secs() as i64) / SECONDS_PER_DAY; + Self::from_days_since_epoch(days) + } + Err(err) => { + let duration = err.duration(); + let days = (duration.as_secs() as i64 + SECONDS_PER_DAY - 1) / SECONDS_PER_DAY; + Self::from_days_since_epoch(-days) + } + } + } + + pub fn from_days_since_epoch(days: i64) -> Option { + let value: i32 = days.try_into().ok()?; + Some(Self { + days_since_epoch: value, + }) + } + + fn components(self) -> (i32, u32, u32) { + civil_from_days(self.days_since_epoch) + } + + fn format_internal(self, pattern: &str) -> String { + let (year, month, day) = self.components(); + let mut output = String::with_capacity(pattern.len() + 8); + let mut chars = pattern.chars(); + while let Some(ch) = chars.next() { + if ch == '%' { + if let Some(spec) = chars.next() { + match spec { + 'Y' => { + let _ = write!(output, "{year:04}"); + } + 'm' => { + let _ = write!(output, "{month:02}"); + } + 'd' => { + let _ = write!(output, "{day:02}"); + } + 'B' => output.push_str(full_month(month)), + 'b' => output.push_str(short_month(month)), + '%' => output.push('%'), + other => { + output.push('%'); + output.push(other); + } + } + } + } else { + output.push(ch); + } + } + output + } +} + +impl Add for NaiveDate { + type Output = Self; + + fn add(self, rhs: Duration) -> Self::Output { + let total = self.days_since_epoch as i64 + rhs.days; + Self::from_days_since_epoch(total).expect("date overflow") + } +} + +impl AddAssign for NaiveDate { + fn add_assign(&mut self, rhs: Duration) { + *self = *self + rhs; + } +} + +impl Sub for NaiveDate { + type Output = Self; + + fn sub(self, rhs: Duration) -> Self::Output { + let total = self.days_since_epoch as i64 - rhs.days; + Self::from_days_since_epoch(total).expect("date overflow") + } +} + +impl SubAssign for NaiveDate { + fn sub_assign(&mut self, rhs: Duration) { + *self = *self - rhs; + } +} + +impl Sub for NaiveDate { + type Output = Duration; + + fn sub(self, rhs: NaiveDate) -> Self::Output { + Duration::days(self.days_since_epoch as i64 - rhs.days_since_epoch as i64) + } +} + +fn days_in_month(year: i32, month: u32) -> u32 { + match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 if is_leap_year(year) => 29, + 2 => 28, + _ => 0, + } +} + +fn is_leap_year(year: i32) -> bool { + (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 +} + +fn days_from_civil(year: i32, month: u32, day: u32) -> Option { + let y = year - if month <= 2 { 1 } else { 0 }; + let era = if y >= 0 { y } else { y - 399 } / 400; + let yoe = y - era * 400; + let mp = month as i32 + if month > 2 { -3 } else { 9 }; + let doy = (153 * mp + 2) / 5 + day as i32 - 1; + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + let days = era * 146097 + doe - 719_468; + days.try_into().ok() +} + +fn civil_from_days(days: i32) -> (i32, u32, u32) { + let days = days as i64 + 719_468; + let era = if days >= 0 { + days / 146_097 + } else { + (days - 146_096) / 146_097 + }; + let doe = days - era * 146_097; + let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; + let mut y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let day = doy - (153 * mp + 2) / 5 + 1; + let month = mp + if mp < 10 { 3 } else { -9 }; + y += if month <= 2 { 1 } else { 0 }; + (y as i32, month as u32, day as u32) +} + +fn full_month(month: u32) -> &'static str { + match month { + 1 => "January", + 2 => "February", + 3 => "March", + 4 => "April", + 5 => "May", + 6 => "June", + 7 => "July", + 8 => "August", + 9 => "September", + 10 => "October", + 11 => "November", + 12 => "December", + _ => "Unknown", + } +} + +fn short_month(month: u32) -> &'static str { + match month { + 1 => "Jan", + 2 => "Feb", + 3 => "Mar", + 4 => "Apr", + 5 => "May", + 6 => "Jun", + 7 => "Jul", + 8 => "Aug", + 9 => "Sep", + 10 => "Oct", + 11 => "Nov", + 12 => "Dec", + _ => "Unk", + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roundtrip_ymd() { + let date = NaiveDate::from_ymd_opt(2024, 6, 30).unwrap(); + assert_eq!(date.year(), 2024); + assert_eq!(date.month(), 6); + assert_eq!(date.day(), 30); + assert_eq!(date.weekday().num_days_from_monday(), 6); + } + + #[test] + fn add_days() { + let mut date = NaiveDate::from_ymd_opt(2024, 1, 31).unwrap(); + date += Duration::days(1); + assert_eq!((date.year(), date.month(), date.day()), (2024, 2, 1)); + } +} diff --git a/src/views/components.rs b/src/views/components.rs index e1b9bf8..72a17d9 100644 --- a/src/views/components.rs +++ b/src/views/components.rs @@ -15,7 +15,7 @@ use crate::components::ui::{ TabsTrigger, Textarea, Toast, ToastViewport, Toggle, ToggleGroup, ToggleGroupItem, ToggleGroupMode, ToggleGroupOrientation, Tooltip, }; -use chrono::NaiveDate; +use crate::time::NaiveDate; use dioxus::html::events::FormEvent; use dioxus::prelude::*; diff --git a/src/views/orders.rs b/src/views/orders.rs index 809f01b..ed980d1 100644 --- a/src/views/orders.rs +++ b/src/views/orders.rs @@ -5,7 +5,7 @@ use crate::components::ui::{ SelectOption, Slider, Table, TableBody, TableCaption, TableCell, TableColumnConfig, TableFooter, TableHead, TableHeader, TableRow, TableRowData, }; -use chrono::NaiveDate; +use crate::time::NaiveDate; use dioxus::prelude::*; const PAGE_SIZE: usize = 8;