refactor: move fundamental combinators from mod.rs to the primitive module

This commit is contained in:
yuankunzhang
2025-06-15 17:12:42 +08:00
parent 9b67307512
commit 6e64f21967
8 changed files with 150 additions and 128 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ use crate::items::space;
use super::{
date::{self, Date},
s,
primitive::s,
time::{self, Time},
};
+1 -1
View File
@@ -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)]
+9 -122
View File
@@ -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<i32> {
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::Offset> {
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<i32, E>
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<u32, E>
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<f64, E>
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<Item> {
trace(
+2 -1
View File
@@ -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<i32> {
alt((text_ordinal, number_ordinal)).parse_next(input)
}
+128
View File
@@ -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<i32, E>
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<u32, E>
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<f64, E>
where
E: ParserError<&'a str>,
{
(opt(one_of(['+', '-'])), digit1, opt(preceded('.', digit1)))
.void()
.take()
.verify_map(|s: &str| s.parse().ok())
.parse_next(input)
}
+4 -1
View File
@@ -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 {
+4 -1
View File
@@ -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 {
+1 -1
View File
@@ -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 {