diff --git a/org_mode_samples/object/timestamp/all_repeater_warning_delay_mark.org b/org_mode_samples/object/timestamp/all_repeater_warning_delay_mark.org new file mode 100644 index 0000000..2e9a867 --- /dev/null +++ b/org_mode_samples/object/timestamp/all_repeater_warning_delay_mark.org @@ -0,0 +1,7 @@ +# All the marks for repeater and warning delay +[1970-01-01 Thu 8:15-13:15foo +1h -2h] +[1970-01-01 Thu 8:15-13:15foo ++1d -2d] +[1970-01-01 Thu 8:15-13:15foo .+1w -2w] +[1970-01-01 Thu 8:15-13:15foo +1m --2m] +[1970-01-01 Thu 8:15-13:15foo ++1y --2y] +[1970-01-01 Thu 8:15-13:15foo .+1d --2h] diff --git a/org_mode_samples/object/timestamp/simple.org b/org_mode_samples/object/timestamp/simple.org index c84b6c7..5f584d7 100644 --- a/org_mode_samples/object/timestamp/simple.org +++ b/org_mode_samples/object/timestamp/simple.org @@ -2,13 +2,17 @@ <%%(foo bar baz)> # active <1970-01-01 Thu 8:15rest +1w -1d> +# Any value for "REST" in the first timestamp makes this a regular timestamp rather than a time range. +<1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d> # inactive [1970-01-01 Thu 8:15rest +1w -1d] +# Any value for "REST" in the first timestamp makes this a regular timestamp rather than a time range. +[1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d] # active date range <1970-01-01 Thu 8:15rest +1w -1d>--<1970-01-01 Thu 8:15rest +1w -1d> # active time range -<1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d> +<1970-01-01 Thu 8:15-13:15otherrest +1w -1d> # inactive date range [1970-01-01 Thu 8:15rest +1w -1d]--[1970-01-01 Thu 8:15rest +1w -1d] # inactive time range -[1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d] +[1970-01-01 Thu 8:15-13:15otherrest +1w -1d] diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 1075383..7138fa1 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -10,10 +10,11 @@ use super::sexp::Token; use super::util::compare_standard_properties; use super::util::get_property; use super::util::get_property_boolean; +use super::util::get_property_numeric; use super::util::get_property_quoted_string; use super::util::get_property_unquoted_atom; -use crate::types::AstNode; use crate::types::AngleLink; +use crate::types::AstNode; use crate::types::BabelCall; use crate::types::Bold; use crate::types::CheckboxType; @@ -23,6 +24,9 @@ use crate::types::Clock; use crate::types::Code; use crate::types::Comment; use crate::types::CommentBlock; +use crate::types::Date; +use crate::types::DayOfMonth; +use crate::types::DayOfMonthInner; use crate::types::DiarySexp; use crate::types::Document; use crate::types::DocumentElement; @@ -39,6 +43,8 @@ use crate::types::GetStandardProperties; use crate::types::GreaterBlock; use crate::types::Heading; use crate::types::HorizontalRule; +use crate::types::Hour; +use crate::types::HourInner; use crate::types::InlineBabelCall; use crate::types::InlineSourceBlock; use crate::types::Italic; @@ -46,6 +52,10 @@ use crate::types::Keyword; use crate::types::LatexEnvironment; use crate::types::LatexFragment; use crate::types::LineBreak; +use crate::types::Minute; +use crate::types::MinuteInner; +use crate::types::Month; +use crate::types::MonthInner; use crate::types::NodeProperty; use crate::types::OrgMacro; use crate::types::Paragraph; @@ -62,6 +72,8 @@ use crate::types::PropertyDrawer; use crate::types::RadioLink; use crate::types::RadioTarget; use crate::types::RegularLink; +use crate::types::RepeaterType; +use crate::types::RepeaterWarningDelayValueType; use crate::types::Section; use crate::types::SrcBlock; use crate::types::StandardProperties; @@ -73,11 +85,18 @@ use crate::types::Table; use crate::types::TableCell; use crate::types::TableRow; use crate::types::Target; +use crate::types::Time; +use crate::types::TimeUnit; use crate::types::Timestamp; +use crate::types::TimestampRangeType; +use crate::types::TimestampType; use crate::types::TodoKeywordType; use crate::types::Underline; use crate::types::Verbatim; use crate::types::VerseBlock; +use crate::types::WarningDelayType; +use crate::types::Year; +use crate::types::YearInner; #[derive(Debug)] pub enum DiffEntry<'b, 's> { @@ -2115,11 +2134,278 @@ fn compare_timestamp<'b, 's>( _source: &'s str, emacs: &'b Token<'s>, rust: &'b Timestamp<'s>, -) -> Result, Box> { - let this_status = DiffStatus::Good; - let message = None; +) -> Result, Box> { + let mut this_status = DiffStatus::Good; + let mut message = None; - // TODO: Compare :type :range-type :raw-value :year-start :month-start :day-start :hour-start :minute-start :year-end :month-end :day-end :hour-end :minute-end + // Compare type + let timestamp_type = get_property_unquoted_atom(emacs, ":type")?; + match (timestamp_type, &rust.timestamp_type) { + (Some("diary"), TimestampType::Diary) => {} + (Some("active"), TimestampType::Active) => {} + (Some("inactive"), TimestampType::Inactive) => {} + (Some("active-range"), TimestampType::ActiveRange) => {} + (Some("inactive-range"), TimestampType::InactiveRange) => {} + _ => { + this_status = DiffStatus::Bad; + message = Some(format!( + "Timestamp type mismatch (emacs != rust) {:?} != {:?}", + timestamp_type, rust.timestamp_type + )); + } + } + + // Compare range-type + let range_type = get_property_unquoted_atom(emacs, ":range-type")?; + match (range_type, &rust.range_type) { + (Some("daterange"), TimestampRangeType::DateRange) => {} + (Some("timerange"), TimestampRangeType::TimeRange) => {} + (None, TimestampRangeType::None) => {} + _ => { + this_status = DiffStatus::Bad; + message = Some(format!( + "Range type mismatch (emacs != rust) {:?} != {:?}", + range_type, rust.range_type + )); + } + } + + // Compare raw-value + let raw_value = get_property_quoted_string(emacs, ":raw-value")? + .ok_or("Timestamps should have a :raw-value.")?; + if raw_value != rust.get_raw_value() { + this_status = DiffStatus::Bad; + message = Some(format!( + "Raw value mismatch (emacs != rust) {:?} != {:?}", + raw_value, + rust.get_raw_value() + )); + } + + // Compare start + let year_start: Option = get_property_numeric(emacs, ":year-start")?; + let month_start: Option = get_property_numeric(emacs, ":month-start")?; + let day_of_month_start: Option = get_property_numeric(emacs, ":day-start")?; + let rust_year_start = rust.start.as_ref().map(Date::get_year).map(Year::get_value); + let rust_month_start = rust + .start + .as_ref() + .map(Date::get_month) + .map(Month::get_value); + let rust_day_of_month_start = rust + .start + .as_ref() + .map(Date::get_day_of_month) + .map(DayOfMonth::get_value); + if year_start != rust_year_start { + this_status = DiffStatus::Bad; + message = Some(format!( + "year start mismatch (emacs != rust) {:?} != {:?}", + year_start, rust_year_start + )); + } + if month_start != rust_month_start { + this_status = DiffStatus::Bad; + message = Some(format!( + "month start mismatch (emacs != rust) {:?} != {:?}", + month_start, rust_month_start + )); + } + if day_of_month_start != rust_day_of_month_start { + this_status = DiffStatus::Bad; + message = Some(format!( + "day of month start mismatch (emacs != rust) {:?} != {:?}", + day_of_month_start, rust_day_of_month_start + )); + } + + // Compare end + let year_end: Option = get_property_numeric(emacs, ":year-end")?; + let month_end: Option = get_property_numeric(emacs, ":month-end")?; + let day_of_month_end: Option = get_property_numeric(emacs, ":day-end")?; + let rust_year_end = rust.end.as_ref().map(Date::get_year).map(Year::get_value); + let rust_month_end = rust.end.as_ref().map(Date::get_month).map(Month::get_value); + let rust_day_of_month_end = rust + .end + .as_ref() + .map(Date::get_day_of_month) + .map(DayOfMonth::get_value); + if year_end != rust_year_end { + this_status = DiffStatus::Bad; + message = Some(format!( + "year end mismatch (emacs != rust) {:?} != {:?}", + year_end, rust_year_end + )); + } + if month_end != rust_month_end { + this_status = DiffStatus::Bad; + message = Some(format!( + "month end mismatch (emacs != rust) {:?} != {:?}", + month_end, rust_month_end + )); + } + if day_of_month_end != rust_day_of_month_end { + this_status = DiffStatus::Bad; + message = Some(format!( + "day of month end mismatch (emacs != rust) {:?} != {:?}", + day_of_month_end, rust_day_of_month_end + )); + } + + // Compare time start + let hour_start: Option = get_property_numeric(emacs, ":hour-start")?; + let minute_start: Option = get_property_numeric(emacs, ":minute-start")?; + let rust_hour_start = rust + .start_time + .as_ref() + .map(Time::get_hour) + .map(Hour::get_value); + let rust_minute_start = rust + .start_time + .as_ref() + .map(Time::get_minute) + .map(Minute::get_value); + if hour_start != rust_hour_start { + this_status = DiffStatus::Bad; + message = Some(format!( + "hour start mismatch (emacs != rust) {:?} != {:?}", + hour_start, rust_hour_start + )); + } + if minute_start != rust_minute_start { + this_status = DiffStatus::Bad; + message = Some(format!( + "minute start mismatch (emacs != rust) {:?} != {:?}", + minute_start, rust_minute_start + )); + } + + // Compare time end + let hour_end: Option = get_property_numeric(emacs, ":hour-end")?; + let minute_end: Option = get_property_numeric(emacs, ":minute-end")?; + let rust_hour_end = rust + .end_time + .as_ref() + .map(Time::get_hour) + .map(Hour::get_value); + let rust_minute_end = rust + .end_time + .as_ref() + .map(Time::get_minute) + .map(Minute::get_value); + if hour_end != rust_hour_end { + this_status = DiffStatus::Bad; + message = Some(format!( + "hour end mismatch (emacs != rust) {:?} != {:?}", + hour_end, rust_hour_end + )); + } + if minute_end != rust_minute_end { + this_status = DiffStatus::Bad; + message = Some(format!( + "minute end mismatch (emacs != rust) {:?} != {:?}", + minute_end, rust_minute_end + )); + } + + // Compare repeater + let repeater_type = get_property_unquoted_atom(emacs, ":repeater-type")?; + let repeater_value: Option = + get_property_numeric(emacs, ":repeater-value")?; + let repeater_unit = get_property_unquoted_atom(emacs, ":repeater-unit")?; + let rust_repeater_type = rust + .repeater + .as_ref() + .map(|repeater| &repeater.repeater_type); + let rust_repeater_value = rust.repeater.as_ref().map(|repeater| repeater.value); + let rust_repeater_unit = rust.repeater.as_ref().map(|repeater| &repeater.unit); + match (repeater_type, rust_repeater_type) { + (Some("cumulate"), Some(RepeaterType::Cumulative)) => {} + (Some("catch-up"), Some(RepeaterType::CatchUp)) => {} + (Some("restart"), Some(RepeaterType::Restart)) => {} + (None, None) => {} + _ => { + this_status = DiffStatus::Bad; + message = Some(format!( + "Repeater type mismatch (emacs != rust) {:?} != {:?}", + repeater_type, rust_repeater_type + )); + } + } + if repeater_value != rust_repeater_value { + this_status = DiffStatus::Bad; + message = Some(format!( + "Repeater value mismatch (emacs != rust) {:?} != {:?}", + repeater_value, rust_repeater_value + )); + } + match (repeater_unit, rust_repeater_unit) { + (Some("hour"), Some(TimeUnit::Hour)) => {} + (Some("day"), Some(TimeUnit::Day)) => {} + (Some("week"), Some(TimeUnit::Week)) => {} + (Some("month"), Some(TimeUnit::Month)) => {} + (Some("year"), Some(TimeUnit::Year)) => {} + (None, None) => {} + _ => { + this_status = DiffStatus::Bad; + message = Some(format!( + "Repeater unit mismatch (emacs != rust) {:?} != {:?}", + repeater_unit, rust_repeater_unit + )); + } + } + + // Compare warning_delay + let warning_delay_type = get_property_unquoted_atom(emacs, ":warning-type")?; + let warning_delay_value: Option = + get_property_numeric(emacs, ":warning-value")?; + let warning_delay_unit = get_property_unquoted_atom(emacs, ":warning-unit")?; + let rust_warning_delay_type = rust + .warning_delay + .as_ref() + .map(|warning_delay| &warning_delay.warning_delay_type); + let rust_warning_delay_value = rust + .warning_delay + .as_ref() + .map(|warning_delay| warning_delay.value); + let rust_warning_delay_unit = rust + .warning_delay + .as_ref() + .map(|warning_delay| &warning_delay.unit); + match (warning_delay_type, rust_warning_delay_type) { + (Some("all"), Some(WarningDelayType::All)) => {} + (Some("first"), Some(WarningDelayType::First)) => {} + (None, None) => {} + _ => { + this_status = DiffStatus::Bad; + message = Some(format!( + "Warning delay type mismatch (emacs != rust) {:?} != {:?}", + warning_delay_type, rust_warning_delay_type + )); + } + } + if warning_delay_value != rust_warning_delay_value { + this_status = DiffStatus::Bad; + message = Some(format!( + "Warning delay value mismatch (emacs != rust) {:?} != {:?}", + warning_delay_value, rust_warning_delay_value + )); + } + match (warning_delay_unit, rust_warning_delay_unit) { + (Some("hour"), Some(TimeUnit::Hour)) => {} + (Some("day"), Some(TimeUnit::Day)) => {} + (Some("week"), Some(TimeUnit::Week)) => {} + (Some("month"), Some(TimeUnit::Month)) => {} + (Some("year"), Some(TimeUnit::Year)) => {} + (None, None) => {} + _ => { + this_status = DiffStatus::Bad; + message = Some(format!( + "Warning delay unit mismatch (emacs != rust) {:?} != {:?}", + warning_delay_unit, rust_warning_delay_unit + )); + } + } Ok(DiffResult { status: this_status, diff --git a/src/compare/elisp_fact.rs b/src/compare/elisp_fact.rs index 6d846e5..6256023 100644 --- a/src/compare/elisp_fact.rs +++ b/src/compare/elisp_fact.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; -use crate::types::AstNode; use crate::types::AngleLink; +use crate::types::AstNode; use crate::types::BabelCall; use crate::types::Bold; use crate::types::Citation; diff --git a/src/compare/util.rs b/src/compare/util.rs index 9c001f2..ff3c471 100644 --- a/src/compare/util.rs +++ b/src/compare/util.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use super::elisp_fact::GetElispFact; use super::sexp::Token; use crate::compare::sexp::unquote; @@ -170,9 +172,7 @@ fn maybe_token_to_usize( /// Get a named property from the emacs token. /// -/// Returns Ok(None) if value is nil. -/// -/// Returns error if the attribute is not specified on the token at all. +/// Returns Ok(None) if value is nil or absent. pub(crate) fn get_property<'b, 's, 'x>( emacs: &'b Token<'s>, key: &'x str, @@ -183,14 +183,12 @@ pub(crate) fn get_property<'b, 's, 'x>( .nth(1) .ok_or("Should have an attributes child.")?; let attributes_map = attributes_child.as_map()?; - let prop = attributes_map - .get(key) - .ok_or(format!("Missing {} attribute.", key))?; - match prop.as_atom() { - Ok("nil") => return Ok(None), + let prop = attributes_map.get(key).map(|token| *token); + match prop.map(|token| token.as_atom()) { + Some(Ok("nil")) => return Ok(None), _ => {} }; - Ok(Some(*prop)) + Ok(prop) } /// Get a named property containing an unquoted atom from the emacs token. @@ -234,3 +232,23 @@ pub(crate) fn get_property_boolean<'b, 's, 'x>( .unwrap_or("nil") != "nil") } + +/// Get a named property containing an unquoted numeric value. +/// +/// Returns None if key is not found. +pub(crate) fn get_property_numeric<'b, 's, 'x, N: FromStr>( + emacs: &'b Token<'s>, + key: &'x str, +) -> Result, Box> +where + ::Err: std::error::Error, + ::Err: 's, +{ + let unparsed_string = get_property(emacs, key)? + .map(Token::as_atom) + .map_or(Ok(None), |r| r.map(Some))?; + let parsed_number = unparsed_string + .map(|val| val.parse::()) + .map_or(Ok(None), |r| r.map(Some))?; + Ok(parsed_number) +} diff --git a/src/error/error.rs b/src/error/error.rs index 8032a25..b7e1481 100644 --- a/src/error/error.rs +++ b/src/error/error.rs @@ -9,6 +9,7 @@ pub enum CustomError { MyError(MyError<&'static str>), Nom(I, ErrorKind), IO(std::io::Error), + BoxedError(Box), } #[derive(Debug)] @@ -36,3 +37,9 @@ impl From<&'static str> for CustomError { CustomError::MyError(MyError(value)) } } + +impl From> for CustomError { + fn from(value: Box) -> Self { + CustomError::BoxedError(value) + } +} diff --git a/src/parser/document.rs b/src/parser/document.rs index a7b8b8b..b49973e 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -20,9 +20,9 @@ use crate::context::RefContext; use crate::error::CustomError; use crate::error::MyError; use crate::error::Res; -use crate::types::AstNode; use crate::parser::org_source::convert_error; use crate::parser::util::blank_line; +use crate::types::AstNode; use crate::types::Document; use crate::types::Object; diff --git a/src/parser/in_buffer_settings.rs b/src/parser/in_buffer_settings.rs index 95618e1..d71e413 100644 --- a/src/parser/in_buffer_settings.rs +++ b/src/parser/in_buffer_settings.rs @@ -11,8 +11,8 @@ use super::OrgSource; use crate::context::HeadlineLevelFilter; use crate::error::CustomError; use crate::error::Res; -use crate::types::AstNode; use crate::settings::GlobalSettings; +use crate::types::AstNode; use crate::types::Document; use crate::types::Keyword; diff --git a/src/parser/org_source.rs b/src/parser/org_source.rs index 8e3a9a6..1aa29a7 100644 --- a/src/parser/org_source.rs +++ b/src/parser/org_source.rs @@ -388,6 +388,7 @@ impl<'s> From>> for CustomError<&'s str> { CustomError::MyError(err) => CustomError::MyError(err.into()), CustomError::Nom(input, error_kind) => CustomError::Nom(input.into(), error_kind), CustomError::IO(err) => CustomError::IO(err), + CustomError::BoxedError(err) => CustomError::BoxedError(err), } } } diff --git a/src/parser/timestamp.rs b/src/parser/timestamp.rs index 1d4e50c..03dd759 100644 --- a/src/parser/timestamp.rs +++ b/src/parser/timestamp.rs @@ -1,10 +1,10 @@ use nom::branch::alt; use nom::bytes::complete::tag; use nom::character::complete::anychar; -use nom::character::complete::digit0; use nom::character::complete::digit1; use nom::character::complete::one_of; use nom::character::complete::space1; +use nom::combinator::map; use nom::combinator::opt; use nom::combinator::recognize; use nom::combinator::verify; @@ -21,7 +21,21 @@ use crate::context::ExitMatcherNode; use crate::context::RefContext; use crate::error::Res; use crate::parser::util::get_consumed; +use crate::types::Date; +use crate::types::DayOfMonth; +use crate::types::Hour; +use crate::types::Minute; +use crate::types::Month; +use crate::types::Repeater; +use crate::types::RepeaterType; +use crate::types::Time; +use crate::types::TimeUnit; use crate::types::Timestamp; +use crate::types::TimestampRangeType; +use crate::types::TimestampType; +use crate::types::WarningDelay; +use crate::types::WarningDelayType; +use crate::types::Year; #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub(crate) fn timestamp<'b, 'g, 'r, 's>( @@ -57,6 +71,14 @@ fn diary_timestamp<'b, 'g, 'r, 's>( remaining, Timestamp { source: source.into(), + timestamp_type: TimestampType::Diary, + range_type: TimestampRangeType::None, + start: None, + end: None, + start_time: None, + end_time: None, + repeater: None, + warning_delay: None, }, )) } @@ -97,17 +119,26 @@ fn active_timestamp<'b, 'g, 'r, 's>( input: OrgSource<'s>, ) -> Res, Timestamp<'s>> { let (remaining, _) = tag("<")(input)?; - let (remaining, _date) = date(context, remaining)?; + let (remaining, start) = date(context, remaining)?; let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Gamma, exit_matcher: &active_time_rest_end, }); let time_context = context.with_additional_node(&time_context); - let (remaining, _time) = - opt(tuple((space1, parser_with_context!(time)(&time_context))))(remaining)?; - let (remaining, _repeater) = + let (remaining, time) = opt(tuple(( + space1, + parser_with_context!(time(true))(&time_context), + )))(remaining)?; + let remaining = if time.is_none() { + // Upstream org-mode accepts malformed timestamps. For example '<2016-02-14 Sun ++y>'. + let (remain, _) = opt(parser_with_context!(time_rest)(&time_context))(remaining)?; + remain + } else { + remaining + }; + let (remaining, repeater) = opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?; - let (remaining, _warning_delay) = opt(tuple(( + let (remaining, warning_delay) = opt(tuple(( space1, parser_with_context!(warning_delay)(context), )))(remaining)?; @@ -121,6 +152,14 @@ fn active_timestamp<'b, 'g, 'r, 's>( remaining, Timestamp { source: source.into(), + timestamp_type: TimestampType::Active, + range_type: TimestampRangeType::None, + start: Some(start.clone()), + end: Some(start), + start_time: time.as_ref().map(|(_, time)| time.clone()), + end_time: time.map(|(_, time)| time), + repeater: repeater.map(|(_, repeater)| repeater), + warning_delay: warning_delay.map(|(_, warning_delay)| warning_delay), }, )) } @@ -131,17 +170,26 @@ fn inactive_timestamp<'b, 'g, 'r, 's>( input: OrgSource<'s>, ) -> Res, Timestamp<'s>> { let (remaining, _) = tag("[")(input)?; - let (remaining, _date) = date(context, remaining)?; + let (remaining, start) = date(context, remaining)?; let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Gamma, exit_matcher: &inactive_time_rest_end, }); let time_context = context.with_additional_node(&time_context); - let (remaining, _time) = - opt(tuple((space1, parser_with_context!(time)(&time_context))))(remaining)?; - let (remaining, _repeater) = + let (remaining, time) = opt(tuple(( + space1, + parser_with_context!(time(true))(&time_context), + )))(remaining)?; + let remaining = if time.is_none() { + // Upstream org-mode accepts malformed timestamps. For example '<2016-02-14 Sun ++y>'. + let (remain, _) = opt(parser_with_context!(time_rest)(&time_context))(remaining)?; + remain + } else { + remaining + }; + let (remaining, repeater) = opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?; - let (remaining, _warning_delay) = opt(tuple(( + let (remaining, warning_delay) = opt(tuple(( space1, parser_with_context!(warning_delay)(context), )))(remaining)?; @@ -155,6 +203,14 @@ fn inactive_timestamp<'b, 'g, 'r, 's>( remaining, Timestamp { source: source.into(), + timestamp_type: TimestampType::Inactive, + range_type: TimestampRangeType::None, + start: Some(start.clone()), + end: Some(start), + start_time: time.as_ref().map(|(_, time)| time.clone()), + end_time: time.map(|(_, time)| time), + repeater: repeater.map(|(_, repeater)| repeater), + warning_delay: warning_delay.map(|(_, warning_delay)| warning_delay), }, )) } @@ -164,10 +220,10 @@ fn active_date_range_timestamp<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Timestamp<'s>> { - let (remaining, _first_timestamp) = active_timestamp(context, input)?; + let (remaining, first_timestamp) = active_timestamp(context, input)?; // TODO: Does the space0 at the end of the active/inactive timestamp parsers cause this to be incorrect? I could use a look-behind to make sure the preceding character is not whitespace let (remaining, _separator) = tag("--")(remaining)?; - let (remaining, _second_timestamp) = active_timestamp(context, remaining)?; + let (remaining, second_timestamp) = active_timestamp(context, remaining)?; let (remaining, _trailing_whitespace) = maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?; @@ -177,6 +233,16 @@ fn active_date_range_timestamp<'b, 'g, 'r, 's>( remaining, Timestamp { source: source.into(), + timestamp_type: TimestampType::ActiveRange, + range_type: TimestampRangeType::DateRange, + start: first_timestamp.start, + end: second_timestamp.end, + start_time: first_timestamp.start_time, + end_time: second_timestamp.end_time, + repeater: first_timestamp.repeater.or(second_timestamp.repeater), + warning_delay: first_timestamp + .warning_delay + .or(second_timestamp.warning_delay), }, )) } @@ -187,7 +253,7 @@ fn active_time_range_timestamp<'b, 'g, 'r, 's>( input: OrgSource<'s>, ) -> Res, Timestamp<'s>> { let (remaining, _) = tag("<")(input)?; - let (remaining, _date) = date(context, remaining)?; + let (remaining, start_date) = date(context, remaining)?; let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Gamma, exit_matcher: &active_time_rest_end, @@ -198,13 +264,15 @@ fn active_time_range_timestamp<'b, 'g, 'r, 's>( exit_matcher: &time_range_rest_end, }); let first_time_context = time_context.with_additional_node(&first_time_context); - let (remaining, _first_time) = - tuple((space1, parser_with_context!(time)(&first_time_context)))(remaining)?; + let (remaining, (_, first_time)) = tuple(( + space1, + parser_with_context!(time(false))(&first_time_context), + ))(remaining)?; let (remaining, _) = tag("-")(remaining)?; - let (remaining, _second_time) = parser_with_context!(time)(&time_context)(remaining)?; - let (remaining, _repeater) = + let (remaining, second_time) = parser_with_context!(time(true))(&time_context)(remaining)?; + let (remaining, repeater) = opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?; - let (remaining, _warning_delay) = opt(tuple(( + let (remaining, warning_delay) = opt(tuple(( space1, parser_with_context!(warning_delay)(context), )))(remaining)?; @@ -218,6 +286,14 @@ fn active_time_range_timestamp<'b, 'g, 'r, 's>( remaining, Timestamp { source: source.into(), + timestamp_type: TimestampType::ActiveRange, + range_type: TimestampRangeType::TimeRange, + start: Some(start_date.clone()), + end: Some(start_date), + start_time: Some(first_time), + end_time: Some(second_time), + repeater: repeater.map(|(_, repeater)| repeater), + warning_delay: warning_delay.map(|(_, warning_delay)| warning_delay), }, )) } @@ -227,10 +303,10 @@ fn inactive_date_range_timestamp<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Timestamp<'s>> { - let (remaining, _first_timestamp) = inactive_timestamp(context, input)?; + let (remaining, first_timestamp) = inactive_timestamp(context, input)?; // TODO: Does the space0 at the end of the active/inactive timestamp parsers cause this to be incorrect? I could use a look-behind to make sure the preceding character is not whitespace let (remaining, _separator) = tag("--")(remaining)?; - let (remaining, _second_timestamp) = inactive_timestamp(context, remaining)?; + let (remaining, second_timestamp) = inactive_timestamp(context, remaining)?; let (remaining, _trailing_whitespace) = maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?; @@ -240,6 +316,17 @@ fn inactive_date_range_timestamp<'b, 'g, 'r, 's>( remaining, Timestamp { source: source.into(), + + timestamp_type: TimestampType::InactiveRange, + range_type: TimestampRangeType::DateRange, + start: first_timestamp.start, + end: second_timestamp.end, + start_time: first_timestamp.start_time, + end_time: second_timestamp.end_time, + repeater: first_timestamp.repeater.or(second_timestamp.repeater), + warning_delay: first_timestamp + .warning_delay + .or(second_timestamp.warning_delay), }, )) } @@ -250,7 +337,7 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>( input: OrgSource<'s>, ) -> Res, Timestamp<'s>> { let (remaining, _) = tag("[")(input)?; - let (remaining, _date) = date(context, remaining)?; + let (remaining, start_date) = date(context, remaining)?; let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Gamma, exit_matcher: &inactive_time_rest_end, @@ -261,13 +348,15 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>( exit_matcher: &time_range_rest_end, }); let first_time_context = time_context.with_additional_node(&first_time_context); - let (remaining, _first_time) = - tuple((space1, parser_with_context!(time)(&first_time_context)))(remaining)?; + let (remaining, (_, first_time)) = tuple(( + space1, + parser_with_context!(time(false))(&first_time_context), + ))(remaining)?; let (remaining, _) = tag("-")(remaining)?; - let (remaining, _second_time) = parser_with_context!(time)(&time_context)(remaining)?; - let (remaining, _repeater) = + let (remaining, second_time) = parser_with_context!(time(true))(&time_context)(remaining)?; + let (remaining, repeater) = opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?; - let (remaining, _warning_delay) = opt(tuple(( + let (remaining, warning_delay) = opt(tuple(( space1, parser_with_context!(warning_delay)(context), )))(remaining)?; @@ -281,6 +370,14 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>( remaining, Timestamp { source: source.into(), + timestamp_type: TimestampType::InactiveRange, + range_type: TimestampRangeType::TimeRange, + start: Some(start_date.clone()), + end: Some(start_date), + start_time: Some(first_time), + end_time: Some(second_time), + repeater: repeater.map(|(_, repeater)| repeater), + warning_delay: warning_delay.map(|(_, warning_delay)| warning_delay), }, )) } @@ -289,18 +386,33 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>( fn date<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, -) -> Res, OrgSource<'s>> { - let (remaining, _year) = verify(digit1, |year: &OrgSource<'_>| year.len() == 4)(input)?; +) -> Res, Date<'s>> { + let (remaining, year) = verify(digit1, |year: &OrgSource<'_>| year.len() == 4)(input)?; let (remaining, _) = tag("-")(remaining)?; - let (remaining, _month) = verify(digit1, |month: &OrgSource<'_>| month.len() == 2)(remaining)?; + let (remaining, month) = verify(digit1, |month: &OrgSource<'_>| month.len() == 2)(remaining)?; let (remaining, _) = tag("-")(remaining)?; - let (remaining, _day_of_month) = verify(digit1, |day_of_month: &OrgSource<'_>| { + let (remaining, day_of_month) = verify(digit1, |day_of_month: &OrgSource<'_>| { day_of_month.len() == 2 })(remaining)?; - let (remaining, _dayname) = + let (remaining, day_name) = opt(tuple((space1, parser_with_context!(dayname)(context))))(remaining)?; - let source = get_consumed(input, remaining); - Ok((remaining, source)) + + let year = Year::new(Into::<&str>::into(year)) + .expect("TODO: I should be able to return CustomError from nom parsers."); + let month = Month::new(Into::<&str>::into(month)) + .expect("TODO: I should be able to return CustomError from nom parsers."); + let day_of_month = DayOfMonth::new(Into::<&str>::into(day_of_month)) + .expect("TODO: I should be able to return CustomError from nom parsers."); + + let date = Date::new( + year, + month, + day_of_month, + day_name.map(|(_, day_name)| Into::<&str>::into(day_name)), + ) + .expect("TODO: I should be able to return CustomError from nom parsers."); + + Ok((remaining, date)) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] @@ -335,20 +447,39 @@ fn dayname_end<'b, 'g, 'r, 's>( }))(input) } +const fn time<'c>( + allow_rest: bool, +) -> impl for<'b, 'g, 'r, 's> Fn(RefContext<'b, 'g, 'r, 's>, OrgSource<'s>) -> Res, Time<'s>> +{ + move |context, input| _time(context, input, allow_rest) +} + #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn time<'b, 'g, 'r, 's>( +fn _time<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, -) -> Res, OrgSource<'s>> { - let (remaining, _hour) = verify(digit1, |hour: &OrgSource<'_>| { + allow_rest: bool, +) -> Res, Time<'s>> { + let (remaining, hour) = verify(digit1, |hour: &OrgSource<'_>| { hour.len() >= 1 && hour.len() <= 2 })(input)?; let (remaining, _) = tag(":")(remaining)?; - let (remaining, _minute) = + let (remaining, minute) = verify(digit1, |minute: &OrgSource<'_>| minute.len() == 2)(remaining)?; - let (remaining, _time_rest) = opt(parser_with_context!(time_rest)(context))(remaining)?; - let source = get_consumed(input, remaining); - Ok((remaining, source)) + let (remaining, time_rest) = if allow_rest { + opt(parser_with_context!(time_rest)(context))(remaining)? + } else { + (remaining, None) + }; + + let hour = Hour::new(Into::<&str>::into(hour)) + .expect("TODO: I should be able to return CustomError from nom parsers."); + let minute = Minute::new(Into::<&str>::into(minute)) + .expect("TODO: I should be able to return CustomError from nom parsers."); + let time = Time::new(hour, minute, time_rest.map(Into::<&str>::into)) + .expect("TODO: I should be able to return CustomError from nom parsers."); + + Ok((remaining, time)) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] @@ -401,8 +532,10 @@ fn time_range_rest_end<'b, 'g, 'r, 's>( ) -> Res, OrgSource<'s>> { // We pop off the most recent context element to get a context tree with just the active/inactive_time_rest_end exit matcher (removing this function from the exit matcher chain) because the 2nd time in the range does not end when a "-TIME" pattern is found. let parent_node = context.get_parent().expect("Two context elements are added to the tree when adding this exit matcher, so it should be impossible for this to return None."); - let exit_contents = - recognize(tuple((tag("-"), parser_with_context!(time)(&parent_node))))(input); + let exit_contents = recognize(tuple(( + tag("-"), + parser_with_context!(time(true))(&parent_node), + )))(input); exit_contents } @@ -410,29 +543,66 @@ fn time_range_rest_end<'b, 'g, 'r, 's>( fn repeater<'b, 'g, 'r, 's>( _context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, -) -> Res, OrgSource<'s>> { +) -> Res, Repeater> { // + for cumulative type // ++ for catch-up type // .+ for restart type - let (remaining, _mark) = alt((tag("++"), tag("+"), tag(".+")))(input)?; - let (remaining, _value) = digit0(remaining)?; + let (remaining, repeater_type) = alt(( + map(tag("++"), |_| RepeaterType::CatchUp), + map(tag("+"), |_| RepeaterType::Cumulative), + map(tag(".+"), |_| RepeaterType::Restart), + ))(input)?; + let (remaining, value) = digit1(remaining)?; + let value = Into::<&str>::into(value) + .parse() + .expect("digit1 ensures this will parse as a number."); // h = hour, d = day, w = week, m = month, y = year - let (remaining, _unit) = recognize(one_of("hdwmy"))(remaining)?; - let source = get_consumed(input, remaining); - Ok((remaining, source)) + let (remaining, unit) = alt(( + map(tag("h"), |_| TimeUnit::Hour), + map(tag("d"), |_| TimeUnit::Day), + map(tag("w"), |_| TimeUnit::Week), + map(tag("m"), |_| TimeUnit::Month), + map(tag("y"), |_| TimeUnit::Year), + ))(remaining)?; + Ok(( + remaining, + Repeater { + repeater_type, + value, + unit, + }, + )) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn warning_delay<'b, 'g, 'r, 's>( _context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, -) -> Res, OrgSource<'s>> { +) -> Res, WarningDelay> { // - for all type // -- for first type - let (remaining, _mark) = alt((tag("--"), tag("-")))(input)?; - let (remaining, _value) = digit0(remaining)?; + let (remaining, warning_delay_type) = alt(( + map(tag("--"), |_| WarningDelayType::First), + map(tag("-"), |_| WarningDelayType::All), + ))(input)?; + let (remaining, value) = digit1(remaining)?; + let value = Into::<&str>::into(value) + .parse() + .expect("digit1 ensures this will parse as a number."); // h = hour, d = day, w = week, m = month, y = year - let (remaining, _unit) = recognize(one_of("hdwmy"))(remaining)?; - let source = get_consumed(input, remaining); - Ok((remaining, source)) + let (remaining, unit) = alt(( + map(tag("h"), |_| TimeUnit::Hour), + map(tag("d"), |_| TimeUnit::Day), + map(tag("w"), |_| TimeUnit::Week), + map(tag("m"), |_| TimeUnit::Month), + map(tag("y"), |_| TimeUnit::Year), + ))(remaining)?; + Ok(( + remaining, + WarningDelay { + warning_delay_type, + value, + unit, + }, + )) } diff --git a/src/types/mod.rs b/src/types/mod.rs index 6f41156..89d5015 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -54,14 +54,23 @@ pub use object::Bold; pub use object::Citation; pub use object::CitationReference; pub use object::Code; +pub use object::Date; +pub use object::DayOfMonth; +pub use object::DayOfMonthInner; pub use object::Entity; pub use object::ExportSnippet; pub use object::FootnoteReference; +pub use object::Hour; +pub use object::HourInner; pub use object::InlineBabelCall; pub use object::InlineSourceBlock; pub use object::Italic; pub use object::LatexFragment; pub use object::LineBreak; +pub use object::Minute; +pub use object::MinuteInner; +pub use object::Month; +pub use object::MonthInner; pub use object::Object; pub use object::OrgMacro; pub use object::PlainLink; @@ -69,13 +78,24 @@ pub use object::PlainText; pub use object::RadioLink; pub use object::RadioTarget; pub use object::RegularLink; +pub use object::Repeater; +pub use object::RepeaterType; +pub use object::RepeaterWarningDelayValueType; pub use object::StatisticsCookie; pub use object::StrikeThrough; pub use object::Subscript; pub use object::Superscript; pub use object::Target; +pub use object::Time; +pub use object::TimeUnit; pub use object::Timestamp; +pub use object::TimestampRangeType; +pub use object::TimestampType; pub use object::Underline; pub use object::Verbatim; +pub use object::WarningDelay; +pub use object::WarningDelayType; +pub use object::Year; +pub use object::YearInner; pub(crate) use source::SetSource; pub use standard_properties::StandardProperties; diff --git a/src/types/object.rs b/src/types/object.rs index 2d8e06e..3440997 100644 --- a/src/types/object.rs +++ b/src/types/object.rs @@ -1,6 +1,7 @@ use super::GetStandardProperties; use super::StandardProperties; +// TODO: Why did we make Object implement PartialEq again? Was it just for tests? #[derive(Debug, PartialEq)] pub enum Object<'s> { Bold(Bold<'s>), @@ -184,6 +185,253 @@ pub struct Superscript<'s> { #[derive(Debug, PartialEq)] pub struct Timestamp<'s> { pub source: &'s str, + pub timestamp_type: TimestampType, + pub range_type: TimestampRangeType, + pub start: Option>, + pub end: Option>, + pub start_time: Option>, + pub end_time: Option>, + pub repeater: Option, + pub warning_delay: Option, +} + +#[derive(Debug, PartialEq)] +pub enum TimestampType { + Diary, + Active, + Inactive, + ActiveRange, + InactiveRange, +} + +#[derive(Debug, PartialEq)] +pub enum TimestampRangeType { + None, + DateRange, + TimeRange, +} + +pub type YearInner = u16; +pub type MonthInner = u8; +pub type DayOfMonthInner = u8; +pub type HourInner = u8; +pub type MinuteInner = u8; + +#[derive(Debug, PartialEq, Clone)] +pub struct Year(YearInner); + +#[derive(Debug, PartialEq, Clone)] +pub struct Month(MonthInner); + +#[derive(Debug, PartialEq, Clone)] +pub struct DayOfMonth(DayOfMonthInner); + +#[derive(Debug, PartialEq, Clone)] +pub struct Hour(HourInner); + +#[derive(Debug, PartialEq, Clone)] +pub struct Minute(MinuteInner); + +impl Year { + // TODO: Make a real error type instead of a boxed any error. + pub fn new<'s>(source: &'s str) -> Result> { + let year = source.parse::()?; + Ok(Year(year)) + } + + pub fn get_value(&self) -> YearInner { + self.0 + } +} + +impl Month { + // TODO: Make a real error type instead of a boxed any error. + pub fn new<'s>(source: &'s str) -> Result> { + let month = source.parse::()?; + if month < 1 || month > 12 { + Err("Month exceeds possible range.")?; + } + Ok(Month(month)) + } + + pub fn get_value(&self) -> MonthInner { + self.0 + } +} + +impl DayOfMonth { + // TODO: Make a real error type instead of a boxed any error. + pub fn new<'s>(source: &'s str) -> Result> { + let day_of_month = source.parse::()?; + if day_of_month < 1 || day_of_month > 31 { + Err("Day of month exceeds possible range.")?; + } + Ok(DayOfMonth(day_of_month)) + } + + pub fn get_value(&self) -> DayOfMonthInner { + self.0 + } +} + +impl Hour { + // TODO: Make a real error type instead of a boxed any error. + pub fn new<'s>(source: &'s str) -> Result> { + let hour = source.parse::()?; + if hour > 23 { + Err("Hour exceeds possible range.")?; + } + Ok(Hour(hour)) + } + + pub fn get_value(&self) -> HourInner { + self.0 + } +} + +impl Minute { + // TODO: Make a real error type instead of a boxed any error. + pub fn new<'s>(source: &'s str) -> Result> { + let minute = source.parse::()?; + if minute > 59 { + Err("Minute exceeds possible range.")?; + } + Ok(Minute(minute)) + } + + pub fn get_value(&self) -> MinuteInner { + self.0 + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Date<'s> { + year: Year, + month: Month, + day_of_month: DayOfMonth, + day_name: Option<&'s str>, +} + +impl<'s> Date<'s> { + // TODO: Make a real error type instead of a boxed any error. + pub fn new( + year: Year, + month: Month, + day_of_month: DayOfMonth, + day_name: Option<&'s str>, + ) -> Result> { + // TODO: Does org-mode support non-gregorian calendars? + // TODO: Do I want to validate leap year? + match (month.get_value(), day_of_month.get_value()) { + (1, 1..=31) => {} + (2, 1..=29) => {} + (3, 1..=31) => {} + (4, 1..=30) => {} + (5, 1..=31) => {} + (6, 1..=30) => {} + (7, 1..=31) => {} + (8, 1..=31) => {} + (9, 1..=30) => {} + (10, 1..=31) => {} + (11, 1..=30) => {} + (12, 1..=31) => {} + _ => Err("Invalid day of month for the month.")?, + }; + Ok(Date { + year, + month, + day_of_month, + day_name, + }) + } + + pub fn get_year(&self) -> &Year { + &self.year + } + + pub fn get_month(&self) -> &Month { + &self.month + } + + pub fn get_day_of_month(&self) -> &DayOfMonth { + &self.day_of_month + } + + pub fn get_day_name(&self) -> Option<&'s str> { + self.day_name + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Time<'s> { + hour: Hour, + minute: Minute, + postfix: Option<&'s str>, +} + +impl<'s> Time<'s> { + // TODO: Make a real error type instead of a boxed any error. + pub fn new( + hour: Hour, + minute: Minute, + postfix: Option<&'s str>, + ) -> Result> { + Ok(Time { + hour, + minute, + postfix, + }) + } + + pub fn get_hour(&self) -> &Hour { + &self.hour + } + + pub fn get_minute(&self) -> &Minute { + &self.minute + } + + pub fn get_postfix(&self) -> Option<&'s str> { + self.postfix + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum RepeaterType { + Cumulative, + CatchUp, + Restart, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum WarningDelayType { + All, + First, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum TimeUnit { + Hour, + Day, + Week, + Month, + Year, +} + +pub type RepeaterWarningDelayValueType = u16; + +#[derive(Debug, PartialEq, Clone)] +pub struct Repeater { + pub repeater_type: RepeaterType, + pub value: RepeaterWarningDelayValueType, + pub unit: TimeUnit, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct WarningDelay { + pub warning_delay_type: WarningDelayType, + pub value: RepeaterWarningDelayValueType, + pub unit: TimeUnit, } impl<'s> GetStandardProperties<'s> for Object<'s> { @@ -381,3 +629,9 @@ impl<'s> StandardProperties<'s> for PlainText<'s> { self.source } } + +impl<'s> Timestamp<'s> { + pub fn get_raw_value(&self) -> &'s str { + self.source.trim_end() + } +}