236 lines
5.4 KiB
Rust
236 lines
5.4 KiB
Rust
//! Parser example for ISO8601 dates. This does not handle the entire specification but it should
|
|
//! show the gist of it and be easy to extend to parse additional forms.
|
|
|
|
use std::{
|
|
env, fmt,
|
|
fs::File,
|
|
io::{self, Read},
|
|
};
|
|
|
|
use combine::{
|
|
choice,
|
|
error::ParseError,
|
|
many, optional,
|
|
parser::char::{char, digit},
|
|
stream::position,
|
|
Parser, Stream,
|
|
};
|
|
|
|
#[cfg(feature = "std")]
|
|
use combine::{
|
|
stream::{easy, position::SourcePosition},
|
|
EasyParser,
|
|
};
|
|
|
|
enum Error<E> {
|
|
Io(io::Error),
|
|
Parse(E),
|
|
}
|
|
|
|
impl<E> fmt::Display for Error<E>
|
|
where
|
|
E: fmt::Display,
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match *self {
|
|
Error::Io(ref err) => write!(f, "{}", err),
|
|
Error::Parse(ref err) => write!(f, "{}", err),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
pub struct Date {
|
|
pub year: i32,
|
|
pub month: i32,
|
|
pub day: i32,
|
|
}
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
pub struct Time {
|
|
pub hour: i32,
|
|
pub minute: i32,
|
|
pub second: i32,
|
|
pub time_zone: i32,
|
|
}
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
pub struct DateTime {
|
|
pub date: Date,
|
|
pub time: Time,
|
|
}
|
|
|
|
fn two_digits<Input>() -> impl Parser<Input, Output = i32>
|
|
where
|
|
Input: Stream<Token = char>,
|
|
// Necessary due to rust-lang/rust#24159
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
{
|
|
(digit(), digit()).map(|(x, y): (char, char)| {
|
|
let x = x.to_digit(10).expect("digit");
|
|
let y = y.to_digit(10).expect("digit");
|
|
(x * 10 + y) as i32
|
|
})
|
|
}
|
|
|
|
/// Parses a time zone
|
|
/// +0012
|
|
/// -06:30
|
|
/// -01
|
|
/// Z
|
|
fn time_zone<Input>() -> impl Parser<Input, Output = i32>
|
|
where
|
|
Input: Stream<Token = char>,
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
{
|
|
let utc = char('Z').map(|_| 0);
|
|
let offset = (
|
|
choice([char('-'), char('+')]),
|
|
two_digits(),
|
|
optional(optional(char(':')).with(two_digits())),
|
|
)
|
|
.map(|(sign, hour, minute)| {
|
|
let offset = hour * 60 + minute.unwrap_or(0);
|
|
if sign == '-' {
|
|
-offset
|
|
} else {
|
|
offset
|
|
}
|
|
});
|
|
|
|
utc.or(offset)
|
|
}
|
|
|
|
/// Parses a date
|
|
/// 2010-01-30
|
|
fn date<Input>() -> impl Parser<Input, Output = Date>
|
|
where
|
|
Input: Stream<Token = char>,
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
{
|
|
(
|
|
many::<String, _, _>(digit()),
|
|
char('-'),
|
|
two_digits(),
|
|
char('-'),
|
|
two_digits(),
|
|
)
|
|
.map(|(year, _, month, _, day)| {
|
|
// Its ok to just unwrap since we only parsed digits
|
|
Date {
|
|
year: year.parse().unwrap(),
|
|
month,
|
|
day,
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Parses a time
|
|
/// 12:30:02
|
|
fn time<Input>() -> impl Parser<Input, Output = Time>
|
|
where
|
|
Input: Stream<Token = char>,
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
{
|
|
(
|
|
two_digits(),
|
|
char(':'),
|
|
two_digits(),
|
|
char(':'),
|
|
two_digits(),
|
|
time_zone(),
|
|
)
|
|
.map(|(hour, _, minute, _, second, time_zone)| {
|
|
// Its ok to just unwrap since we only parsed digits
|
|
Time {
|
|
hour,
|
|
minute,
|
|
second,
|
|
time_zone,
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Parses a date time according to ISO8601
|
|
/// 2015-08-02T18:54:42+02
|
|
fn date_time<Input>() -> impl Parser<Input, Output = DateTime>
|
|
where
|
|
Input: Stream<Token = char>,
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
{
|
|
(date(), char('T'), time()).map(|(date, _, time)| DateTime { date, time })
|
|
}
|
|
|
|
#[test]
|
|
fn test() {
|
|
// A parser for
|
|
let result = date_time().parse("2015-08-02T18:54:42+02");
|
|
let d = DateTime {
|
|
date: Date {
|
|
year: 2015,
|
|
month: 8,
|
|
day: 2,
|
|
},
|
|
time: Time {
|
|
hour: 18,
|
|
minute: 54,
|
|
second: 42,
|
|
time_zone: 2 * 60,
|
|
},
|
|
};
|
|
assert_eq!(result, Ok((d, "")));
|
|
|
|
let result = date_time().parse("50015-12-30T08:54:42Z");
|
|
let d = DateTime {
|
|
date: Date {
|
|
year: 50015,
|
|
month: 12,
|
|
day: 30,
|
|
},
|
|
time: Time {
|
|
hour: 8,
|
|
minute: 54,
|
|
second: 42,
|
|
time_zone: 0,
|
|
},
|
|
};
|
|
assert_eq!(result, Ok((d, "")));
|
|
}
|
|
|
|
fn main() {
|
|
let result = match env::args().nth(1) {
|
|
Some(file) => File::open(file).map_err(Error::Io).and_then(main_),
|
|
None => main_(io::stdin()),
|
|
};
|
|
match result {
|
|
Ok(_) => println!("OK"),
|
|
Err(err) => println!("{}", err),
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "std")]
|
|
fn main_<R>(mut read: R) -> Result<(), Error<easy::Errors<char, String, SourcePosition>>>
|
|
where
|
|
R: Read,
|
|
{
|
|
let mut text = String::new();
|
|
read.read_to_string(&mut text).map_err(Error::Io)?;
|
|
date_time()
|
|
.easy_parse(position::Stream::new(&*text))
|
|
.map_err(|err| Error::Parse(err.map_range(|s| s.to_string())))?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(feature = "std"))]
|
|
fn main_<R>(mut read: R) -> Result<(), Error<::combine::error::StringStreamError>>
|
|
where
|
|
R: Read,
|
|
{
|
|
let mut text = String::new();
|
|
read.read_to_string(&mut text).map_err(Error::Io)?;
|
|
date_time()
|
|
.parse(position::Stream::new(&*text))
|
|
.map_err(Error::Parse)?;
|
|
Ok(())
|
|
}
|