blob: 3daaa72dcac9969810ccb2b267dd22838c4a7c73 [file] [log] [blame]
//! 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(())
}