#![deny(unsafe_code)]
#![warn(
clippy::cognitive_complexity,
clippy::dbg_macro,
clippy::debug_assert_with_mut_call,
clippy::doc_link_with_quotes,
clippy::doc_markdown,
clippy::empty_line_after_outer_attr,
clippy::empty_structs_with_brackets,
clippy::float_cmp,
clippy::float_cmp_const,
clippy::float_equality_without_abs,
keyword_idents,
clippy::missing_const_for_fn,
missing_copy_implementations,
missing_debug_implementations,
clippy::missing_docs_in_private_items,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
non_ascii_idents,
noop_method_call,
clippy::option_if_let_else,
clippy::print_stderr,
clippy::print_stdout,
clippy::semicolon_if_nothing_returned,
clippy::unseparated_literal_suffix,
clippy::shadow_unrelated,
clippy::similar_names,
clippy::suspicious_operation_groupings,
unused_crate_dependencies,
unused_extern_crates,
unused_import_braces,
clippy::unused_self,
clippy::use_debug,
clippy::used_underscore_binding,
clippy::useless_let_if_seq,
clippy::wildcard_dependencies,
clippy::wildcard_imports
)]
pub(crate) mod temporal;
#[cfg(feature = "wasm")]
pub mod wasm;
use std::str::FromStr;
use jiff::{civil::DateTime, Span, Zoned};
use lazy_regex::regex;
use temporal::find_datetime;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct NewEvent {
pub summary: String,
pub time: DateTime,
pub location: Option<String>,
pub duration: Option<Span>
}
impl NewEvent {
pub fn parse_at_time(s: &str, now: Zoned) -> Result<Self, EventParseError> {
let mut summary: Option<String> = None;
let mut location: Option<String> = None;
let (time, time_starts, time_ends) = find_datetime(s, now)?.ok_or(EventParseError::MissingTime)?;
let (before_time, _) = s.split_at(time_starts);
let (_, after_time) = s.split_at(time_ends);
let before_time_trimmed = before_time.trim();
if !before_time_trimmed.is_empty() {
summary = Some(before_time_trimmed.to_owned());
}
let location_start_pattern = regex!(r"\s*[@ | ,]\s+.+");
if location_start_pattern.is_match(after_time) {
let trimmed_location = after_time.trim().trim_start_matches(['@', ',']).trim_start();
location = Some(trimmed_location.to_owned());
}
Ok(Self {
summary: summary.ok_or(EventParseError::MissingSummary)?,
time,
location,
duration: None
})
}
}
#[derive(Debug, PartialEq, Clone, Copy, thiserror::Error, Serialize, Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum EventParseError {
#[error("Missing time")]
MissingTime,
#[error("Invalid time")]
InvalidTime,
#[error("Ambiguous time")]
AmbiguousTime,
#[error("Missing summary")]
MissingSummary,
#[error("Ambiguous duration")]
AmbiguousDuration,
}
impl FromStr for NewEvent {
type Err = EventParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let now = Zoned::now();
Self::parse_at_time(s, now)
}
}
#[cfg(test)]
mod tests {
use super::*;
use jiff::{civil::date, Zoned};
#[test]
fn fail_only_summary() {
let event = "John's birthday".parse::<NewEvent>();
assert_eq!(event, Err(EventParseError::MissingTime));
}
#[test]
fn trivial_a() {
let now = date(2024, 6, 1).intz("UTC").unwrap();
let event = NewEvent::parse_at_time("John's birthday 18.11.", now).unwrap();
assert_eq!(event.summary, "John's birthday");
assert_eq!(event.time.year(), Zoned::now().year());
assert_eq!(event.time.day(), 18);
assert_eq!(event.time.month(), 11);
assert_eq!(event.time.hour(), 0);
assert_eq!(event.location, None);
}
#[test]
fn with_time_short() {
let now = date(2024, 6, 1).intz("UTC").unwrap();
let event = NewEvent::parse_at_time("John's birthday 18.11. 16", now).unwrap();
assert_eq!(event.summary, "John's birthday");
assert_eq!(event.time.year(), Zoned::now().year());
assert_eq!(event.time.day(), 18);
assert_eq!(event.time.month(), 11);
assert_eq!(event.time.hour(), 16);
assert_eq!(event.time.minute(), 0);
assert_eq!(event.location, None);
}
#[test]
fn with_time_long_a() {
let now = date(2024, 6, 1).intz("UTC").unwrap();
let event = NewEvent::parse_at_time("John's birthday 18.11. 16:00", now).unwrap();
assert_eq!(event.summary, "John's birthday");
assert_eq!(event.time.year(), Zoned::now().year());
assert_eq!(event.time.day(), 18);
assert_eq!(event.time.month(), 11);
assert_eq!(event.time.hour(), 16);
assert_eq!(event.time.minute(), 0);
assert_eq!(event.location, None);
}
#[test]
fn with_time_long_b() {
let now = date(2024, 6, 1).intz("UTC").unwrap();
let event = NewEvent::parse_at_time("John's birthday 18.11. 1:59", now).unwrap();
assert_eq!(event.summary, "John's birthday");
assert_eq!(event.time.year(), Zoned::now().year());
assert_eq!(event.time.day(), 18);
assert_eq!(event.time.month(), 11);
assert_eq!(event.time.hour(), 1);
assert_eq!(event.time.minute(), 59);
assert_eq!(event.location, None);
}
#[test]
fn trivial_with_location_a() {
let now = date(2024, 6, 1).intz("UTC").unwrap();
let event = NewEvent::parse_at_time("John's birthday 18.11. @ Memory Plaza", now).unwrap();
assert_eq!(event.summary, "John's birthday");
assert_eq!(event.time.year(), Zoned::now().year());
assert_eq!(event.time.day(), 18);
assert_eq!(event.time.month(), 11);
assert_eq!(event.location, Some("Memory Plaza".to_owned()));
}
#[test]
fn relative_a() {
let now = date(2024, 6, 1).intz("UTC").unwrap();
let event = NewEvent::parse_at_time("John's birthday tomorrow", now).unwrap();
assert_eq!(event.summary, "John's birthday");
assert_eq!(event.time.year(), 2024);
assert_eq!(event.time.month(), 6);
assert_eq!(event.time.day(), 2);
assert_eq!(event.location, None);
}
#[test]
fn relative_with_location_a() {
let now = date(2024, 6, 1).intz("UTC").unwrap();
let event = NewEvent::parse_at_time("John's birthday tomorrow @ Tuomiokirkko", now).unwrap();
assert_eq!(event.summary, "John's birthday");
assert_eq!(event.time.year(), 2024);
assert_eq!(event.time.month(), 6);
assert_eq!(event.time.day(), 2);
assert_eq!(event.location, Some("Tuomiokirkko".to_owned()));
}
#[test]
fn relative_with_location_b() {
let now = date(2024, 6, 1).intz("UTC").unwrap();
let event = NewEvent::parse_at_time("John's birthday tomorrow, Temppeliaukion Kirkko", now).unwrap();
assert_eq!(event.summary, "John's birthday");
assert_eq!(event.time.year(), 2024);
assert_eq!(event.time.month(), 6);
assert_eq!(event.time.day(), 2);
assert_eq!(event.location, Some("Temppeliaukion Kirkko".to_owned()));
}
}