├── .gitignore ├── src ├── serde │ ├── mod.rs │ ├── time.rs │ ├── date.rs │ └── datetime.rs ├── sqlx │ ├── postgres │ │ ├── mod.rs │ │ ├── time.rs │ │ ├── date.rs │ │ └── datetime.rs │ └── mod.rs ├── util │ ├── date │ │ ├── mod.rs │ │ ├── manipulate.rs │ │ ├── validate.rs │ │ └── convert.rs │ ├── time │ │ ├── mod.rs │ │ ├── validate.rs │ │ ├── manipulate.rs │ │ └── convert.rs │ ├── mod.rs │ ├── leap.rs │ ├── offset.rs │ ├── constants.rs │ └── format.rs ├── local │ ├── mod.rs │ ├── data_block.rs │ ├── cursor.rs │ ├── header.rs │ ├── errors.rs │ └── transition_rule.rs ├── errors │ ├── invalid_format.rs │ ├── mod.rs │ └── out_of_range.rs ├── offset.rs ├── lib.rs ├── shared.rs ├── cron.rs └── date.rs ├── .env.example ├── .cargo └── audit.toml ├── migrations └── 01_astrolabe_tests.sql ├── tests ├── compose.yml ├── shared.rs ├── serde.rs ├── errors.rs ├── cron.rs ├── sqlx-postgres.rs ├── time.rs └── date.rs ├── .github └── workflows │ ├── audit.yml │ └── checks.yml ├── LICENSE-MIT ├── Cargo.toml ├── assets └── logo.svg ├── README.md ├── CHANGELOG.md └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .env 4 | -------------------------------------------------------------------------------- /src/serde/mod.rs: -------------------------------------------------------------------------------- 1 | mod date; 2 | mod datetime; 3 | mod time; 4 | -------------------------------------------------------------------------------- /src/sqlx/postgres/mod.rs: -------------------------------------------------------------------------------- 1 | mod date; 2 | mod datetime; 3 | mod time; 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://astrolabe:password@localhost:5432/astrolabe 2 | -------------------------------------------------------------------------------- /src/util/date/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod convert; 2 | pub(crate) mod manipulate; 3 | pub(crate) mod validate; 4 | -------------------------------------------------------------------------------- /src/util/time/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod convert; 2 | pub(crate) mod manipulate; 3 | pub(crate) mod validate; 4 | -------------------------------------------------------------------------------- /.cargo/audit.toml: -------------------------------------------------------------------------------- 1 | [advisories] 2 | # https://rustsec.org/advisories/RUSTSEC-2023-0071.html 3 | ignore = ["RUSTSEC-2023-0071"] 4 | -------------------------------------------------------------------------------- /src/sqlx/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "sqlx-postgres")] 2 | #[cfg_attr(docsrs, doc(cfg(feature = "sqlx-postgres")))] 3 | mod postgres; 4 | -------------------------------------------------------------------------------- /src/local/mod.rs: -------------------------------------------------------------------------------- 1 | mod cursor; 2 | mod data_block; 3 | mod errors; 4 | mod header; 5 | pub(crate) mod timezone; 6 | mod transition_rule; 7 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod constants; 2 | pub(crate) mod date; 3 | pub(crate) mod format; 4 | pub(crate) mod leap; 5 | pub(crate) mod offset; 6 | pub(crate) mod parse; 7 | pub(crate) mod time; 8 | -------------------------------------------------------------------------------- /migrations/01_astrolabe_tests.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE astrolabe_tests ( 2 | timestamp TIMESTAMP, 3 | timestamps TIMESTAMP[], 4 | date DATE, 5 | dates DATE[], 6 | time TIME, 7 | times TIME[] 8 | ); 9 | -------------------------------------------------------------------------------- /tests/compose.yml: -------------------------------------------------------------------------------- 1 | name: astrolabe 2 | services: 3 | db: 4 | image: postgres:17.1-alpine3.20 5 | restart: always 6 | environment: 7 | - POSTGRES_USER=astrolabe 8 | - POSTGRES_PASSWORD=password 9 | ports: 10 | - '5432:5432' 11 | -------------------------------------------------------------------------------- /tests/shared.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod shared_tests { 3 | use astrolabe::Precision; 4 | 5 | #[test] 6 | fn precision() { 7 | let precision = Precision::Seconds; 8 | // Debug 9 | println!("{:?}", precision); 10 | // Clone 11 | #[allow(clippy::redundant_clone)] 12 | let clone = precision.clone(); 13 | // PartialEq 14 | assert!(precision == clone); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: audit 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * 1,4' 6 | push: 7 | paths: 8 | - 'Cargo.toml' 9 | - '.github/workflows/audit.yml' 10 | pull_request: 11 | paths: 12 | - 'Cargo.toml' 13 | - '.github/workflows/audit.yml' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | audit: 18 | uses: giyomoon/workflows/.github/workflows/rust-audit.yml@main 19 | with: 20 | rust-version: nightly 21 | -------------------------------------------------------------------------------- /src/errors/invalid_format.rs: -------------------------------------------------------------------------------- 1 | use super::AstrolabeError; 2 | use std::fmt; 3 | 4 | /// An error indicating that the string to be parsed is invalid. 5 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 6 | pub struct InvalidFormat(String); 7 | 8 | impl fmt::Display for InvalidFormat { 9 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 10 | write!(f, "{}", self.0) 11 | } 12 | } 13 | 14 | pub(crate) fn create_invalid_format(message: String) -> AstrolabeError { 15 | AstrolabeError::InvalidFormat(InvalidFormat(message)) 16 | } 17 | -------------------------------------------------------------------------------- /src/util/time/validate.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{out_of_range::create_simple_oor, AstrolabeError}; 2 | 3 | pub(crate) fn validate_time(hour: u32, minute: u32, second: u32) -> Result<(), AstrolabeError> { 4 | if hour > 23 { 5 | return Err(create_simple_oor("hour", 0, 23, hour as i128)); 6 | } 7 | 8 | if minute > 59 { 9 | return Err(create_simple_oor("minute", 0, 59, minute as i128)); 10 | } 11 | 12 | if second > 59 { 13 | return Err(create_simple_oor("second", 0, 59, second as i128)); 14 | }; 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /src/util/leap.rs: -------------------------------------------------------------------------------- 1 | /// Returns leap years between the year 0001 and the given year (exluding the year itself) 2 | pub(crate) fn leap_years(mut year: i32) -> u32 { 3 | if year.is_positive() { 4 | year -= 1; 5 | } 6 | if year.is_negative() { 7 | year += 1; 8 | } 9 | let year_abs = year.abs(); 10 | let mut leaps = year_abs / 4 - year_abs / 100 + year_abs / 400; 11 | if year.is_negative() { 12 | leaps += 1; 13 | } 14 | leaps as u32 15 | } 16 | 17 | /// Checks if the given year is a leap year 18 | pub(crate) fn is_leap_year(mut year: i32) -> bool { 19 | if year.is_negative() { 20 | year += 1; 21 | } 22 | year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2024 Jasmin Noetzli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/serde/time.rs: -------------------------------------------------------------------------------- 1 | use crate::Time; 2 | use serde::de; 3 | use serde::ser; 4 | use std::fmt; 5 | 6 | /// Serialize a [`Time`] instance as `HH:mm:ss`. 7 | impl ser::Serialize for Time { 8 | fn serialize(&self, serializer: S) -> Result 9 | where 10 | S: ser::Serializer, 11 | { 12 | serializer.serialize_str(&self.format("HH:mm:ss")) 13 | } 14 | } 15 | 16 | struct TimeVisitor; 17 | 18 | impl de::Visitor<'_> for TimeVisitor { 19 | type Value = Time; 20 | 21 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 22 | formatter.write_str("a formatted date string in the format `HH:mm:ss`") 23 | } 24 | 25 | fn visit_str(self, value: &str) -> Result 26 | where 27 | E: de::Error, 28 | { 29 | value.parse().map_err(E::custom) 30 | } 31 | } 32 | 33 | /// Deserialize a `HH:mm:ss` formatted string into a [`Time`] instance. 34 | impl<'de> de::Deserialize<'de> for Time { 35 | fn deserialize(deserializer: D) -> Result 36 | where 37 | D: serde::Deserializer<'de>, 38 | { 39 | deserializer.deserialize_str(TimeVisitor) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/sqlx/postgres/time.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use crate::{util::constants::BUG_MSG, Time}; 4 | use sqlx::{ 5 | encode::IsNull, 6 | error::BoxDynError, 7 | postgres::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef}, 8 | Decode, Encode, Postgres, Type, 9 | }; 10 | 11 | impl Type for Time { 12 | fn type_info() -> PgTypeInfo { 13 | PgTypeInfo::with_name("time") 14 | } 15 | } 16 | 17 | impl PgHasArrayType for Time { 18 | fn array_type_info() -> PgTypeInfo { 19 | PgTypeInfo::with_name("time[]") 20 | } 21 | } 22 | 23 | impl Encode<'_, Postgres> for Time { 24 | fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { 25 | let micros = (self.as_nanos() / 1_000) as i64; 26 | Encode::::encode(micros, buf) 27 | } 28 | 29 | fn size_hint(&self) -> usize { 30 | mem::size_of::() 31 | } 32 | } 33 | 34 | impl Decode<'_, Postgres> for Time { 35 | fn decode(value: PgValueRef<'_>) -> Result { 36 | let micros: i64 = Decode::::decode(value)?; 37 | Ok(Time::from_nanos(micros as u64 * 1_000).expect(BUG_MSG)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/serde/date.rs: -------------------------------------------------------------------------------- 1 | use crate::Date; 2 | use serde::de; 3 | use serde::ser; 4 | use std::fmt; 5 | 6 | /// Serialize a [`Date`] instance as `yyyy-MM-dd`. 7 | impl ser::Serialize for Date { 8 | fn serialize(&self, serializer: S) -> Result 9 | where 10 | S: ser::Serializer, 11 | { 12 | serializer.serialize_str(&self.format("yyyy-MM-dd")) 13 | } 14 | } 15 | 16 | struct DateVisitor; 17 | 18 | impl de::Visitor<'_> for DateVisitor { 19 | type Value = Date; 20 | 21 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 22 | formatter.write_str("a formatted date string in the format `yyyy-MM-dd`") 23 | } 24 | 25 | fn visit_str(self, value: &str) -> Result 26 | where 27 | E: de::Error, 28 | { 29 | value.parse().map_err(E::custom) 30 | } 31 | } 32 | 33 | /// Deserialize a `yyyy-MM-dd` formatted string into a [`Date`] instance. 34 | impl<'de> de::Deserialize<'de> for Date { 35 | fn deserialize(deserializer: D) -> Result 36 | where 37 | D: serde::Deserializer<'de>, 38 | { 39 | deserializer.deserialize_str(DateVisitor) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "astrolabe" 3 | version = "0.5.4" 4 | edition = "2021" 5 | description = "Date and time library for Rust. Aims to be feature rich, lightweight and easy-to-use." 6 | homepage = "https://github.com/giyomoon/astrolabe" 7 | documentation = "https://docs.rs/astrolabe/" 8 | repository = "https://github.com/giyomoon/astrolabe" 9 | readme = "README.md" 10 | authors = ["Jasmin "] 11 | license = "MIT OR Apache-2.0" 12 | keywords = ["date", "time"] 13 | categories = ["date-and-time"] 14 | rust-version = "1.56" 15 | include = [ 16 | "src/**", 17 | "Cargo.toml", 18 | "README.md", 19 | "LICENSE-APACHE", 20 | "LICENSE-MIT", 21 | ] 22 | 23 | [package.metadata.docs.rs] 24 | all-features = true 25 | rustdoc-args = ["--cfg", "docsrs"] 26 | 27 | [features] 28 | sqlx-postgres = ["sqlx", "sqlx/postgres"] 29 | 30 | [dependencies.serde] 31 | version = "1.0" 32 | default-features = false 33 | optional = true 34 | 35 | [dependencies.sqlx] 36 | version = "^0.8.1" 37 | default-features = false 38 | optional = true 39 | 40 | [dev-dependencies] 41 | serde_test = "1.0" 42 | 43 | [dev-dependencies.sqlx] 44 | version = "^0.8.1" 45 | default-features = false 46 | features = ["runtime-tokio-rustls", "migrate", "macros", "postgres"] 47 | -------------------------------------------------------------------------------- /src/serde/datetime.rs: -------------------------------------------------------------------------------- 1 | use crate::DateTime; 2 | use crate::Precision; 3 | use serde::de; 4 | use serde::ser; 5 | use std::fmt; 6 | 7 | /// Serialize a [`DateTime`] instance as an RFC 3339 string. 8 | impl ser::Serialize for DateTime { 9 | fn serialize(&self, serializer: S) -> Result 10 | where 11 | S: ser::Serializer, 12 | { 13 | serializer.serialize_str(&self.format_rfc3339(Precision::Seconds)) 14 | } 15 | } 16 | 17 | struct DateTimeVisitor; 18 | 19 | impl de::Visitor<'_> for DateTimeVisitor { 20 | type Value = DateTime; 21 | 22 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 23 | formatter.write_str("an RFC 3339 formatted date string") 24 | } 25 | 26 | fn visit_str(self, value: &str) -> Result 27 | where 28 | E: de::Error, 29 | { 30 | value.parse().map_err(E::custom) 31 | } 32 | } 33 | 34 | /// Deserialize an RFC 3339 string into a [`DateTime`] instance. 35 | impl<'de> de::Deserialize<'de> for DateTime { 36 | fn deserialize(deserializer: D) -> Result 37 | where 38 | D: serde::Deserializer<'de>, 39 | { 40 | deserializer.deserialize_str(DateTimeVisitor) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/errors/mod.rs: -------------------------------------------------------------------------------- 1 | //! Various error types returned by functions in the astrolabe crate. 2 | 3 | pub(crate) mod invalid_format; 4 | pub(crate) mod out_of_range; 5 | pub use self::{invalid_format::InvalidFormat, out_of_range::OutOfRange}; 6 | use std::{error, fmt}; 7 | 8 | /// Custom error enum for the astrolabe crate. 9 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 10 | pub enum AstrolabeError { 11 | /// An error indicating that some given parameter is out of range or resulted in an out of range date/time value. 12 | OutOfRange(OutOfRange), 13 | /// An error indicating that the string to be parsed is invalid. 14 | InvalidFormat(InvalidFormat), 15 | } 16 | 17 | impl fmt::Display for AstrolabeError { 18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | match self { 20 | Self::OutOfRange(e) => e.fmt(f), 21 | Self::InvalidFormat(e) => e.fmt(f), 22 | } 23 | } 24 | } 25 | 26 | impl error::Error for AstrolabeError {} 27 | 28 | impl From for String { 29 | fn from(e: AstrolabeError) -> Self { 30 | e.to_string() 31 | } 32 | } 33 | 34 | impl From<&AstrolabeError> for String { 35 | fn from(e: &AstrolabeError) -> Self { 36 | e.to_string() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/serde.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | #[cfg(feature = "serde")] 3 | mod serde_tests { 4 | use astrolabe::{Date, DateTime, Time}; 5 | use serde_test::{assert_de_tokens_error, assert_tokens, Token}; 6 | 7 | #[test] 8 | fn time() { 9 | let time = Time::from_hms(12, 32, 10).unwrap(); 10 | 11 | assert_tokens(&time, &[Token::String("12:32:10")]); 12 | 13 | assert_de_tokens_error::