Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export default {
},
],
},
setupFilesAfterEnv: ['./jest.setup.ts'],
} satisfies JestConfigWithTsJest;
/* eslint-enable import-x/no-default-export -- Required by Jest */
48 changes: 48 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { iterableEquality } from '@jest/expect-utils';

interface ToPlain {
toPlain(): unknown;
}

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace -- Required for jest
namespace jest {
interface Matchers<R> {
toEqualPlain<E extends ToPlain>(expected: E | undefined): R;
toEqualPlain<E extends ToPlain>(expected: E[]): R;
}
}
}

expect.extend({
toEqualPlain(
received: ToPlain[] | ToPlain | undefined,
expected: ToPlain[] | ToPlain | undefined,
) {
let pass = false;
let expectedPlain: unknown;
let receivedPlain: unknown;

if (Array.isArray(received) && Array.isArray(expected)) {
receivedPlain = received.map((r) => r.toPlain());
expectedPlain = expected.map((e) => e.toPlain());

pass = this.equals(receivedPlain, expectedPlain, [iterableEquality]);
} else if (!Array.isArray(received) && !Array.isArray(expected)) {
receivedPlain = received?.toPlain();
expectedPlain = expected?.toPlain();

pass = this.equals(receivedPlain, expectedPlain);
} else {
throw new Error(
'Both received and expected values must be of the same type, either ToPlain or ToPlain[].',
);
}

return {
message: () =>
`Expected plain does not match received plain:\r\n\r\n${this.utils.printDiffOrStringify(expectedPlain, receivedPlain, 'Expected', 'Received', true)}`,
pass,
};
},
});
1 change: 1 addition & 0 deletions lib/rrule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ pub mod n_weekday;
pub mod rdate;
pub mod rrule;
pub mod rrule_set;
pub mod time;
pub mod weekday;
200 changes: 113 additions & 87 deletions lib/rrule/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,61 @@ use chrono::Datelike;
use chrono::TimeZone;
use chrono::Timelike;

use crate::rrule::time::Time;

#[derive(Clone)]
pub struct DateTime {
year: u32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
utc: bool,
pub year: u32,
pub month: u32,
pub day: u32,
pub time: Option<Time>,
}

impl DateTime {
pub fn utc(&self) -> bool {
self.utc
}

pub fn to_datetime(
&self,
timezone: &chrono_tz::Tz,
) -> Result<chrono::DateTime<chrono_tz::Tz>, String> {
let timezone = match self.utc {
true => &chrono_tz::Tz::UTC,
false => &timezone,
let timezone = match &self.time {
Some(time) => match time.utc {
true => &chrono_tz::Tz::UTC,
false => &timezone,
},
None => &timezone,
};

let (hour, minute, second) = match &self.time {
Some(time) => (time.hour, time.minute, time.second),
None => (0, 0, 0),
};

match timezone
.with_ymd_and_hms(
self.year as i32,
self.month,
self.day,
self.hour,
self.minute,
self.second,
)
.with_ymd_and_hms(self.year as i32, self.month, self.day, hour, minute, second)
.single()
{
Some(datetime) => Ok(datetime),
None => Err(format!(
"Invalid datetime: {}-{}-{} {}:{}:{} {}",
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second,
if self.utc { "UTC" } else { "Local" }
)),
None => Err(format!("Invalid datetime: {}", self.to_string())),
}
}

pub fn to_string(&self) -> String {
format!(
"{:04}{:02}{:02}T{:02}{:02}{:02}{}",
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second,
if self.utc { "Z" } else { "" }
)
match &self.time {
Some(time) => format!(
"{:04}{:02}{:02}T{:02}{:02}{:02}{}",
self.year,
self.month,
self.day,
time.hour,
time.minute,
time.second,
if time.utc { "Z" } else { "" }
),
None => format!("{:04}{:02}{:02}", self.year, self.month, self.day),
}
}

fn from_str(str: &str) -> Result<Self, String> {
if str.len() > 16 || str.len() < 15 {
if !(str.len() == 8 || (str.len() <= 16 && str.len() >= 15)) {
return Err(format!("Invalid datetime string: {}", str));
}

Expand All @@ -85,43 +75,53 @@ impl DateTime {
let month: u32 = month
.parse()
.map_err(|_| format!("Invalid month: {}", month))?;

let day = str
.get(6..8)
.ok_or(format!("Can not extract day from: {}", str))?;
let day: u32 = day.parse().map_err(|_| format!("Invalid day: {}", day))?;

let hour = str
.get(9..11)
.ok_or(format!("Can not extract hour from: {}", str))?;
let hour: u32 = hour
.parse()
.map_err(|_| format!("Invalid hour: {}", hour))?;

let minute = str
.get(11..13)
.ok_or(format!("Can not extract minute from: {}", str))?;
let minute: u32 = minute
.parse()
.map_err(|_| format!("Invalid minute: {}", minute))?;

let second = str
.get(13..15)
.ok_or(format!("Can not extract second from: {}", str))?;
let second: u32 = second
.parse()
.map_err(|_| format!("Invalid second: {}", second))?;

let utc = str.get(15..16).unwrap_or("").to_uppercase() == "Z";
if str.len() > 8 {
let hour = str
.get(9..11)
.ok_or(format!("Can not extract hour from: {}", str))?;
let hour: u32 = hour
.parse()
.map_err(|_| format!("Invalid hour: {}", hour))?;

let minute = str
.get(11..13)
.ok_or(format!("Can not extract minute from: {}", str))?;
let minute: u32 = minute
.parse()
.map_err(|_| format!("Invalid minute: {}", minute))?;

let second = str
.get(13..15)
.ok_or(format!("Can not extract second from: {}", str))?;
let second: u32 = second
.parse()
.map_err(|_| format!("Invalid second: {}", second))?;

let utc = str.get(15..16).unwrap_or("").to_uppercase() == "Z";

return Ok(Self {
year,
month,
day,
time: Some(Time {
hour,
minute,
second,
utc,
}),
});
}

Ok(Self {
year,
month,
day,
hour,
minute,
second,
utc,
time: None,
})
}
}
Expand All @@ -134,20 +134,36 @@ impl From<i64> for DateTime {
let hour = ((numeric / 100000) % 100) as u32;
let minute = ((numeric / 1000) % 100) as u32;
let second = ((numeric / 10) % 100) as u32;
let utc = (numeric % 10) == 1;
let mode = (numeric % 10) as u32;

let time = match mode {
0 => Some(Time {
hour,
minute,
second,
utc: false,
}),
1 => Some(Time {
hour,
minute,
second,
utc: true,
}),
_ => None,
};

DateTime {
year,
month,
day,
hour,
minute,
second,
utc,
time,
}
}
}

// TODO: chrono datetime is alwats converted into DateTime with Time
// Probabbly there should be a method to convert into DateTime without Time
// And this trait must me removed
impl From<&chrono::DateTime<chrono_tz::Tz>> for DateTime {
fn from(datetime: &chrono::DateTime<chrono_tz::Tz>) -> Self {
let year = datetime.year() as u32;
Expand All @@ -162,10 +178,12 @@ impl From<&chrono::DateTime<chrono_tz::Tz>> for DateTime {
year,
month,
day,
hour,
minute,
second,
utc,
time: Some(Time {
hour,
minute,
second,
utc,
}),
}
}
}
Expand All @@ -184,10 +202,12 @@ impl From<&chrono::DateTime<rrule::Tz>> for DateTime {
year,
month,
day,
hour,
minute,
second,
utc,
time: Some(Time {
hour,
minute,
second,
utc,
}),
}
}
}
Expand All @@ -197,10 +217,16 @@ impl Into<i64> for &DateTime {
let year = self.year as i64;
let month = self.month as i64;
let day = self.day as i64;
let hour = self.hour as i64;
let minute = self.minute as i64;
let second = self.second as i64;
let utc = if self.utc { 1 } else { 0 };

let (hour, minute, second, utc) = match &self.time {
Some(time) => (
time.hour as i64,
time.minute as i64,
time.second as i64,
if time.utc { 1 } else { 0 },
),
None => (0, 0, 0, 2),
};

year * 100000000000
+ month * 1000000000
Expand Down
11 changes: 5 additions & 6 deletions lib/rrule/dtstart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@ impl DtStart {
let mut parameters = Parameters::new();

if let Some(tzid) = self.tzid {
// UTC datetimes MUST NOT contain a TZID
if !self.datetime.utc() {
parameters.insert("TZID".to_string(), tzid.to_string());
}
parameters.insert("TZID".to_string(), tzid.to_string());
}

let value: String = self.datetime.to_string();
Expand All @@ -46,8 +43,10 @@ impl DtStart {
}

pub fn new(datetime: DateTime, tzid: Option<chrono_tz::Tz>) -> Result<Self, String> {
if !datetime.utc() && tzid.is_none() {
return Err("TZID is requred for non-UTC DTSTART".to_string());
if let Some(time) = &datetime.time {
if !time.utc && tzid.is_none() {
return Err("TZID is requred for non-UTC DTSTART".to_string());
}
}

Ok(Self { datetime, tzid })
Expand Down
5 changes: 1 addition & 4 deletions lib/rrule/exdate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ impl ExDate {
let mut parameters = Parameters::new();

if let Some(tzid) = self.tzid {
// UTC datetimes MUST NOT contain a TZID
if !self.datetimes.iter().any(|datetime| datetime.utc()) {
parameters.insert("TZID".to_string(), tzid.to_string());
}
parameters.insert("TZID".to_string(), tzid.to_string());
}

let value: String = self
Expand Down
5 changes: 1 addition & 4 deletions lib/rrule/rdate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ impl RDate {
let mut parameters = Parameters::new();

if let Some(tzid) = self.tzid {
// UTC datetimes MUST NOT contain a TZID
if !self.datetimes.iter().any(|datetime| datetime.utc()) {
parameters.insert("TZID".to_string(), tzid.to_string());
}
parameters.insert("TZID".to_string(), tzid.to_string());
}

let value: String = self
Expand Down
Loading
Loading