Skip to content

Commit

Permalink
✨ System timezone on UNIX systems (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
GiyoMoon authored Oct 8, 2023
1 parent 985b2b2 commit 9408b7b
Show file tree
Hide file tree
Showing 26 changed files with 2,637 additions and 1,120 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [0.5.0] - 2023-10-08
Astrolabe can now get the local timezone offset on UNIX systems.
```rust
use astrolabe::{DateTime, Offset, OffsetUtilities, Precision};

// Equivalent to `DateTime::now().set_offset(Offset::Local)`
let now = DateTime::now_local();
// Prints for example:
// 2023-10-08T08:30:00+02:00
println!("{}", now.format_rfc3339(Precision::Seconds));
assert_eq!(Offset::Local, now.get_offset());
```

Also, we decided to panic in case of a date overflow. This should not happen if the library is used correctly, as the valid date range is between `30. June -5879611`..=`12. July 5879611`. This change makes the API much easier to use, as many functions only returned a `Result` because of this edge case.

### Added
- Enums
- [`Offset`](https://docs.rs/astrolabe/0.5.0/astrolabe/enum.Offset.html) - Represents an offset from UTC. `Fixed` or `Local`

### Changed
- Updated the [`OffsetUtilities`](https://docs.rs/astrolabe/0.5.0/astrolabe/trait.OffsetUtilities.html) trait to use the [`Offset`](https://docs.rs/astrolabe/0.5.0/astrolabe/enum.Offset.html) enum
- The `add_*` and `sub_*` functions from the [`DateUtilities`](https://docs.rs/astrolabe/0.5.0/astrolabe/trait.DateUtilities.html) and [`TimeUtilities`](https://docs.rs/astrolabe/0.5.0/astrolabe/trait.TimeUtilities.html) traits now return `Self` instead of `Result`
- `from_timestamp` from the [`DateUtilities`](https://docs.rs/astrolabe/0.5.0/astrolabe/trait.DateUtilities.html) trait now return `Self` instead of `Result`

## [0.4.0] - 2023-05-18
### Added
- Structs
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ homepage = "https://github.com/giyomoon/astrolabe"
documentation = "https://docs.rs/astrolabe/"
repository = "https://github.com/giyomoon/astrolabe"
readme = "README.md"
authors = ["Jasmin <[email protected]>"]
authors = ["Jasmin <[email protected]>"]
license = "MIT OR Apache-2.0"
keywords = ["date", "time"]
categories = ["date-and-time"]
Expand Down
2 changes: 1 addition & 1 deletion LICENSE-APACHE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2022 GiyoMoon
Copyright 2022-2023 Jasmin Noetzli

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion LICENSE-MIT
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022 GiyoMoon
Copyright (c) 2022-2023 Jasmin Noetzli

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
54 changes: 48 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@
Crate
</a>
<span> | </span>
<a href="#example">
Example
<a href="#examples">
Examples
</a>
</h4>
</div>
Expand All @@ -72,10 +72,12 @@ Astrolabe is a date and time library for Rust which aims to be feature rich, lig
- **Manipulation** functions to add, subtract, set and clear date units
- **Cron** expression parser
- **Timezone** offset
- **Local** timezone on UNIX platforms
- **Zero** dependencies
- **Serde** serializing and deserializing (With feature flag `serde`)

## Example
## Examples
### Basic
A basic example which demonstrates creating, formatting and manipulating a `DateTime` instance.

```rust
Expand All @@ -89,15 +91,55 @@ assert_eq!("2022/05/02", date_time.format("yyyy/MM/dd"));

// Create a new instance with a modified DateTime
// The previous instance is not modified and is still in scope
let modified_dt = date_time
.add_hours(11).unwrap()
.add_minutes(23).unwrap();
let modified_dt = date_time.add_hours(11).add_minutes(23);

assert_eq!("2022/05/02 11:23:00", modified_dt.format("yyyy/MM/dd HH:mm:ss"));
assert_eq!("2022-05-02T11:23:00Z", modified_dt.format_rfc3339(Precision::Seconds));
```
To see all implementations for the `DateTime` struct, check out it's [documentation](https://docs.rs/astrolabe/latest/astrolabe/struct.DateTime.html).

### Local timezone (UNIX systems only)
Astrolabe can parse the timezone from `/etc/localtime` to get the local UTC offset. This only works on UNIX systems.

```rust
use astrolabe::{DateTime, Offset, OffsetUtilities, Precision};

// Equivalent to `DateTime::now().set_offset(Offset::Local)`
let now = DateTime::now_local();

// Prints for example:
// 2023-10-08T08:30:00+02:00
println!("{}", now.format_rfc3339(Precision::Seconds));
assert_eq!(Offset::Local, now.get_offset());
```
See [`Offset`](https://docs.rs/astrolabe/latest/astrolabe/enum.Offset.html)

### CRON parsing
```rust
use astrolabe::CronSchedule;

// Every 5 minutes
let schedule = CronSchedule::parse("*/5 * * * *").unwrap();
for date in schedule.take(3) {
println!("{}", date);
}
// Prints for example:
// 2022-05-02 16:15:00
// 2022-05-02 16:20:00
// 2022-05-02 16:25:00

// Every weekday at 10:00
let schedule = CronSchedule::parse("0 10 * * Mon-Fri").unwrap();
for date in schedule.take(3) {
println!("{}", date.format("yyyy-MM-dd HH:mm:ss eeee"));
}
// Prints for example:
// 2022-05-03 10:00:00 Tuesday
// 2022-05-04 10:00:00 Wednesday
// 2022-05-05 10:00:00 Thursday
```
See [`CronSchedule`](https://docs.rs/astrolabe/latest/astrolabe/struct.CronSchedule.html)

## MSRV
This crate uses the Rust 2021 Edition and requires at least version `1.56`.

Expand Down
34 changes: 13 additions & 21 deletions src/cron.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,14 +225,14 @@ impl Iterator for CronSchedule {
_ => now,
};

let mut next = last.add_minutes(1).ok()?;
let mut next = last.add_minutes(1);

let dom_restricted = self.days_of_month.len() != 31;
let dow_restricted = self.days_of_week.len() != 7;

loop {
if !self.months.contains(&(next.month() as u8)) {
next = next.add_months(1).ok()?.clear_until_day();
next = next.add_months(1).clear_until_day();
continue;
}

Expand All @@ -252,17 +252,17 @@ impl Iterator for CronSchedule {
&& !self.days_of_month.contains(&day_of_month))
|| (dow_restricted && !dom_restricted && !self.days_of_week.contains(&day_of_week))
{
next = next.add_days(1).ok()?.clear_until_hour();
next = next.add_days(1).clear_until_hour();
continue;
}

if !self.hours.contains(&(next.hour() as u8)) {
next = next.add_hours(1).ok()?.clear_until_minute();
next = next.add_hours(1).clear_until_minute();
continue;
}

if !self.minutes.contains(&(next.minute() as u8)) {
next = next.add_minutes(1).ok()?.clear_until_second();
next = next.add_minutes(1).clear_until_second();
continue;
}

Expand Down Expand Up @@ -306,7 +306,7 @@ fn parse_cron_part(
} else if let Some(step) = part.strip_prefix("*/") {
let step: u8 = step
.parse()
.map_err(|_| format!("Can't parse step value to u8: {step}"))?;
.map_err(|_| format!("Can't parse step value to u8: {}", step))?;
if step == 0 {
return Err("Step value can't be 0".to_string());
}
Expand Down Expand Up @@ -368,7 +368,7 @@ fn parse_value(value: &str, cron_type: &CronPartType) -> Result<u8, String> {
CronPartType::DayOfWeek if value == "7" => 0,
_ => value
.parse::<u8>()
.map_err(|_| format!("Can't parse value to u8: {value}"))?,
.map_err(|_| format!("Can't parse value to u8: {}", value))?,
})
}

Expand Down Expand Up @@ -479,15 +479,14 @@ mod cron_tests {
"2022/01/20 00:00:00",
];
cron_next("0 0 20 * mon", expected, now);
}

// Test if iterator returns none at overflow
#[test]
#[should_panic]
// Test if iterator returns none at overflow
fn overflow() {
let now = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 0).unwrap();
cron_next_none("* * * * *", now);
let now = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 58, 0).unwrap();
cron_next_none("* * * 8 *", now);
cron_next_none("* * 13 * *", now);
cron_next_none("* 0 * * *", now);
cron_next_none("0 * * * *", now);
CronSchedule::parse("* * * * *", Some(now)).unwrap().next();
}

fn cron_next(cron: &str, expected: Vec<&str>, now: DateTime) {
Expand All @@ -498,11 +497,4 @@ mod cron_tests {
assert_eq!(expected, next.format("yyyy/MM/dd HH:mm:ss"));
}
}

fn cron_next_none(cron: &str, now: DateTime) {
assert!(CronSchedule::parse(cron, Some(now))
.unwrap()
.next()
.is_none());
}
}
87 changes: 64 additions & 23 deletions src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use std::{
/// See the [`DateUtilities`](#impl-DateUtilities-for-Date) implementation for get, set and manipulation methods.
///
/// Range: `30. June -5879611`..=`12. July 5879611`. Please note that year 0 does not exist. After year -1 follows year 1.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
pub struct Date {
pub(crate) days: i32,
}
Expand Down Expand Up @@ -123,7 +123,7 @@ impl Date {

// Use day of year if present, otherwise use month + day of month
Ok(if date.day_of_year.is_some() {
let days = year_doy_to_days(date.year.unwrap_or(1), date.day_of_year.unwrap())?;
let days = year_doy_to_days(date.year.unwrap_or(1), date.day_of_year.unwrap(), false)?;
Self { days }
} else {
Self::from_ymd(
Expand Down Expand Up @@ -250,7 +250,7 @@ impl DateUtilities for Date {
days_to_wday(self.days, false) as u8
}

fn from_timestamp(timestamp: i64) -> Result<Self, AstrolabeError> {
fn from_timestamp(timestamp: i64) -> Self {
let days = (timestamp / SECS_PER_DAY_U64 as i64 + DAYS_TO_1970_I64
- i64::from(
timestamp.is_negative() && timestamp.unsigned_abs() % SECS_PER_DAY_U64 != 0,
Expand All @@ -265,9 +265,14 @@ impl DateUtilities for Date {
- 1,
timestamp as i128,
)
})?;
});

Ok(Self { days })
let days = match days {
Ok(days) => days,
Err(e) => panic!("{}", e),
};

Self { days }
}

fn timestamp(&self) -> i64 {
Expand All @@ -294,34 +299,70 @@ impl DateUtilities for Date {
Ok(Self { days: new_days })
}

fn add_years(&self, years: u32) -> Result<Self, AstrolabeError> {
let new_days = add_years(self.days, years)?;
Ok(Self { days: new_days })
fn add_years(&self, years: u32) -> Self {
let new_days = add_years(self.days, years);

let new_days = match new_days {
Ok(new_days) => new_days,
Err(e) => panic!("{}", e),
};

Self { days: new_days }
}

fn add_months(&self, months: u32) -> Result<Self, AstrolabeError> {
let new_days = add_months(self.days, months)?;
Ok(Self { days: new_days })
fn add_months(&self, months: u32) -> Self {
let new_days = add_months(self.days, months);

let new_days = match new_days {
Ok(new_days) => new_days,
Err(e) => panic!("{}", e),
};

Self { days: new_days }
}

fn add_days(&self, days: u32) -> Result<Self, AstrolabeError> {
let new_days = add_days(self.days, days)?;
Ok(Self { days: new_days })
fn add_days(&self, days: u32) -> Self {
let new_days = add_days(self.days, days);

let new_days = match new_days {
Ok(new_days) => new_days,
Err(e) => panic!("{}", e),
};

Self { days: new_days }
}

fn sub_years(&self, years: u32) -> Result<Self, AstrolabeError> {
let new_days = sub_years(self.days, years)?;
Ok(Self { days: new_days })
fn sub_years(&self, years: u32) -> Self {
let new_days = sub_years(self.days, years);

let new_days = match new_days {
Ok(new_days) => new_days,
Err(e) => panic!("{}", e),
};

Self { days: new_days }
}

fn sub_months(&self, months: u32) -> Result<Self, AstrolabeError> {
let new_days = sub_months(self.days, months)?;
Ok(Self { days: new_days })
fn sub_months(&self, months: u32) -> Self {
let new_days = sub_months(self.days, months);

let new_days = match new_days {
Ok(new_days) => new_days,
Err(e) => panic!("{}", e),
};

Self { days: new_days }
}

fn sub_days(&self, days: u32) -> Result<Self, AstrolabeError> {
let new_days = sub_days(self.days, days)?;
Ok(Self { days: new_days })
fn sub_days(&self, days: u32) -> Self {
let new_days = sub_days(self.days, days);

let new_days = match new_days {
Ok(new_days) => new_days,
Err(e) => panic!("{}", e),
};

Self { days: new_days }
}

fn clear_until_year(&self) -> Self {
Expand Down
Loading

0 comments on commit 9408b7b

Please sign in to comment.