From 6e64f21967b604294e37010ec9293c39df4028a5 Mon Sep 17 00:00:00 2001 From: yuankunzhang Date: Sun, 15 Jun 2025 17:12:42 +0800 Subject: [PATCH] refactor: move fundamental combinators from mod.rs to the primitive module --- src/items/combined.rs | 2 +- src/items/date.rs | 2 +- src/items/mod.rs | 131 +++-------------------------------------- src/items/ordinal.rs | 3 +- src/items/primitive.rs | 128 ++++++++++++++++++++++++++++++++++++++++ src/items/relative.rs | 5 +- src/items/time.rs | 5 +- src/items/weekday.rs | 2 +- 8 files changed, 150 insertions(+), 128 deletions(-) create mode 100644 src/items/primitive.rs diff --git a/src/items/combined.rs b/src/items/combined.rs index c1df933..d452196 100644 --- a/src/items/combined.rs +++ b/src/items/combined.rs @@ -22,7 +22,7 @@ use crate::items::space; use super::{ date::{self, Date}, - s, + primitive::s, time::{self, Time}, }; diff --git a/src/items/date.rs b/src/items/date.rs index 86cc992..b99dc12 100644 --- a/src/items/date.rs +++ b/src/items/date.rs @@ -35,7 +35,7 @@ use winnow::{ ModalResult, Parser, }; -use super::{dec_uint, s}; +use super::primitive::{dec_uint, s}; use crate::ParseDateTimeError; #[derive(PartialEq, Eq, Clone, Debug, Default)] diff --git a/src/items/mod.rs b/src/items/mod.rs index 39fd695..28e4aa7 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -30,6 +30,7 @@ mod combined; mod date; mod ordinal; +mod primitive; mod relative; mod time; mod weekday; @@ -37,16 +38,18 @@ mod weekday; mod epoch { use winnow::{combinator::preceded, ModalResult, Parser}; - use super::{dec_int, s}; + use super::primitive::{dec_int, s}; + pub fn parse(input: &mut &str) -> ModalResult { s(preceded("@", dec_int)).parse_next(input) } } mod timezone { - use super::time; use winnow::ModalResult; + use super::time; + pub(crate) fn parse(input: &mut &str) -> ModalResult { time::timezone(input) } @@ -55,12 +58,11 @@ mod timezone { use chrono::NaiveDate; use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike}; +use primitive::space; use winnow::{ - ascii::{digit1, multispace0}, - combinator::{alt, delimited, not, opt, peek, preceded, repeat, separated, trace}, - error::{AddContext, ContextError, ErrMode, ParserError, StrContext, StrContextValue}, - stream::{AsChar, Stream}, - token::{none_of, one_of, take_while}, + combinator::{alt, trace}, + error::{AddContext, ContextError, ErrMode, StrContext, StrContextValue}, + stream::Stream, ModalResult, Parser, }; @@ -78,121 +80,6 @@ pub enum Item { TimeZone(time::Offset), } -/// Allow spaces and comments before a parser -/// -/// Every token parser should be wrapped in this to allow spaces and comments. -/// It is only preceding, because that allows us to check mandatory whitespace -/// after running the parser. -fn s<'a, O, E>(p: impl Parser<&'a str, O, E>) -> impl Parser<&'a str, O, E> -where - E: ParserError<&'a str>, -{ - preceded(space, p) -} - -/// Parse the space in-between tokens -/// -/// You probably want to use the [`s`] combinator instead. -fn space<'a, E>(input: &mut &'a str) -> winnow::Result<(), E> -where - E: ParserError<&'a str>, -{ - separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input) -} - -/// A hyphen or plus is ignored when it is not followed by a digit -/// -/// This includes being followed by a comment! Compare these inputs: -/// ```txt -/// - 12 weeks -/// - (comment) 12 weeks -/// ``` -/// The last comment should be ignored. -/// -/// The plus is undocumented, but it seems to be ignored. -fn ignored_hyphen_or_plus<'a, E>(input: &mut &'a str) -> winnow::Result<(), E> -where - E: ParserError<&'a str>, -{ - ( - alt(('-', '+')), - multispace0, - peek(not(take_while(1, AsChar::is_dec_digit))), - ) - .void() - .parse_next(input) -} - -/// Parse a comment -/// -/// A comment is given between parentheses, which must be balanced. Any other -/// tokens can be within the comment. -fn comment<'a, E>(input: &mut &'a str) -> winnow::Result<(), E> -where - E: ParserError<&'a str>, -{ - delimited( - '(', - repeat(0.., alt((none_of(['(', ')']).void(), comment))), - ')', - ) - .parse_next(input) -} - -/// Parse a signed decimal integer. -/// -/// Rationale for not using `winnow::ascii::dec_int`: When upgrading winnow from -/// 0.5 to 0.7, we discovered that `winnow::ascii::dec_int` now accepts only the -/// following two forms: -/// -/// - 0 -/// - [+-]?[1-9][0-9]* -/// -/// Inputs like [+-]?0[0-9]* (e.g., `+012`) are therefore rejected. We provide a -/// custom implementation to support such zero-prefixed integers. -fn dec_int<'a, E>(input: &mut &'a str) -> winnow::Result -where - E: ParserError<&'a str>, -{ - (opt(one_of(['+', '-'])), digit1) - .void() - .take() - .verify_map(|s: &str| s.parse().ok()) - .parse_next(input) -} - -/// Parse an unsigned decimal integer. -/// -/// See the rationale for `dec_int` for why we don't use -/// `winnow::ascii::dec_uint`. -fn dec_uint<'a, E>(input: &mut &'a str) -> winnow::Result -where - E: ParserError<&'a str>, -{ - digit1 - .void() - .take() - .verify_map(|s: &str| s.parse().ok()) - .parse_next(input) -} - -/// Parse a float number. -/// -/// Rationale for not using `winnow::ascii::float`: the `float` parser provided -/// by winnow accepts E-notation numbers (e.g., `1.23e4`), whereas GNU date -/// rejects such numbers. To remain compatible with GNU date, we provide a -/// custom implementation that only accepts inputs like [+-]?[0-9]+(\.[0-9]+)?. -fn float<'a, E>(input: &mut &'a str) -> winnow::Result -where - E: ParserError<&'a str>, -{ - (opt(one_of(['+', '-'])), digit1, opt(preceded('.', digit1))) - .void() - .take() - .verify_map(|s: &str| s.parse().ok()) - .parse_next(input) -} - // Parse an item pub fn parse_one(input: &mut &str) -> ModalResult { trace( diff --git a/src/items/ordinal.rs b/src/items/ordinal.rs index 73cd36b..7de4f6f 100644 --- a/src/items/ordinal.rs +++ b/src/items/ordinal.rs @@ -1,13 +1,14 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use super::{dec_uint, s}; use winnow::{ ascii::alpha1, combinator::{alt, opt}, ModalResult, Parser, }; +use super::primitive::{dec_uint, s}; + pub fn ordinal(input: &mut &str) -> ModalResult { alt((text_ordinal, number_ordinal)).parse_next(input) } diff --git a/src/items/primitive.rs b/src/items/primitive.rs new file mode 100644 index 0000000..c1f5180 --- /dev/null +++ b/src/items/primitive.rs @@ -0,0 +1,128 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Primitive combinators. + +use winnow::{ + ascii::{digit1, multispace0}, + combinator::{alt, delimited, not, opt, peek, preceded, repeat, separated}, + error::ParserError, + stream::AsChar, + token::{none_of, one_of, take_while}, + Parser, +}; + +/// Allow spaces and comments before a parser +/// +/// Every token parser should be wrapped in this to allow spaces and comments. +/// It is only preceding, because that allows us to check mandatory whitespace +/// after running the parser. +pub(super) fn s<'a, O, E>(p: impl Parser<&'a str, O, E>) -> impl Parser<&'a str, O, E> +where + E: ParserError<&'a str>, +{ + preceded(space, p) +} + +/// Parse the space in-between tokens +/// +/// You probably want to use the [`s`] combinator instead. +pub(super) fn space<'a, E>(input: &mut &'a str) -> winnow::Result<(), E> +where + E: ParserError<&'a str>, +{ + separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input) +} + +/// A hyphen or plus is ignored when it is not followed by a digit +/// +/// This includes being followed by a comment! Compare these inputs: +/// ```txt +/// - 12 weeks +/// - (comment) 12 weeks +/// ``` +/// The last comment should be ignored. +/// +/// The plus is undocumented, but it seems to be ignored. +fn ignored_hyphen_or_plus<'a, E>(input: &mut &'a str) -> winnow::Result<(), E> +where + E: ParserError<&'a str>, +{ + ( + alt(('-', '+')), + multispace0, + peek(not(take_while(1, AsChar::is_dec_digit))), + ) + .void() + .parse_next(input) +} + +/// Parse a comment +/// +/// A comment is given between parentheses, which must be balanced. Any other +/// tokens can be within the comment. +fn comment<'a, E>(input: &mut &'a str) -> winnow::Result<(), E> +where + E: ParserError<&'a str>, +{ + delimited( + '(', + repeat(0.., alt((none_of(['(', ')']).void(), comment))), + ')', + ) + .parse_next(input) +} + +/// Parse a signed decimal integer. +/// +/// Rationale for not using `winnow::ascii::dec_int`: When upgrading winnow from +/// 0.5 to 0.7, we discovered that `winnow::ascii::dec_int` now accepts only the +/// following two forms: +/// +/// - 0 +/// - [+-]?[1-9][0-9]* +/// +/// Inputs like [+-]?0[0-9]* (e.g., `+012`) are therefore rejected. We provide a +/// custom implementation to support such zero-prefixed integers. +pub(super) fn dec_int<'a, E>(input: &mut &'a str) -> winnow::Result +where + E: ParserError<&'a str>, +{ + (opt(one_of(['+', '-'])), digit1) + .void() + .take() + .verify_map(|s: &str| s.parse().ok()) + .parse_next(input) +} + +/// Parse an unsigned decimal integer. +/// +/// See the rationale for `dec_int` for why we don't use +/// `winnow::ascii::dec_uint`. +pub(super) fn dec_uint<'a, E>(input: &mut &'a str) -> winnow::Result +where + E: ParserError<&'a str>, +{ + digit1 + .void() + .take() + .verify_map(|s: &str| s.parse().ok()) + .parse_next(input) +} + +/// Parse a float number. +/// +/// Rationale for not using `winnow::ascii::float`: the `float` parser provided +/// by winnow accepts E-notation numbers (e.g., `1.23e4`), whereas GNU date +/// rejects such numbers. To remain compatible with GNU date, we provide a +/// custom implementation that only accepts inputs like [+-]?[0-9]+(\.[0-9]+)?. +pub(super) fn float<'a, E>(input: &mut &'a str) -> winnow::Result +where + E: ParserError<&'a str>, +{ + (opt(one_of(['+', '-'])), digit1, opt(preceded('.', digit1))) + .void() + .take() + .verify_map(|s: &str| s.parse().ok()) + .parse_next(input) +} diff --git a/src/items/relative.rs b/src/items/relative.rs index 701a197..6830082 100644 --- a/src/items/relative.rs +++ b/src/items/relative.rs @@ -37,7 +37,10 @@ use winnow::{ ModalResult, Parser, }; -use super::{float, ordinal::ordinal, s}; +use super::{ + ordinal::ordinal, + primitive::{float, s}, +}; #[derive(Clone, Copy, Debug, PartialEq)] pub enum Relative { diff --git a/src/items/time.rs b/src/items/time.rs index 7fa8d3f..ad23a2c 100644 --- a/src/items/time.rs +++ b/src/items/time.rs @@ -52,7 +52,10 @@ use winnow::{ use crate::ParseDateTimeError; -use super::{dec_uint, relative, s}; +use super::{ + primitive::{dec_uint, s}, + relative, +}; #[derive(PartialEq, Debug, Clone, Default)] pub struct Offset { diff --git a/src/items/weekday.rs b/src/items/weekday.rs index 15fff59..138e14c 100644 --- a/src/items/weekday.rs +++ b/src/items/weekday.rs @@ -23,7 +23,7 @@ use winnow::{ascii::alpha1, combinator::opt, seq, ModalResult, Parser}; -use super::{ordinal::ordinal, s}; +use super::{ordinal::ordinal, primitive::s}; #[derive(PartialEq, Eq, Debug)] pub(crate) enum Day {