ISO 8601 Date/Time Parser
Parse ISO 8601 date, time, and datetime strings - the international standard for date and time representations used across systems and languages.
Key Concepts
- calendar dates
- week dates
- ordinal dates
- timezone handling
- duration parsing
Why Parse This Format?
ISO 8601 is the international standard for date and time interchange. It's used in APIs, databases, logs, and any system that needs unambiguous date/time representation across cultures and timezones.
Use Cases
- API date/time parsing and validation
- Database timestamp handling
- Log file timestamp parsing
- Calendar application development
- Cross-system data interchange
Input Format
ISO 8601 supports multiple formats: calendar dates (YYYY-MM-DD), week dates (YYYY-Www-D), ordinal dates (YYYY-DDD), times with optional fractions and timezones, and durations (PnYnMnDTnHnMnS).
YYYY-MM-DD | YYYY-Www-D | YYYY-DDD | HH:MM:SS | YYYY-MM-DDTHH:MM:SSZ | PnYnMnDTnHnMnSExamples
2024-01-15VALIDCalendar date
20240115VALIDCompact calendar date (no separators)
2024-W02-1VALIDWeek date (2nd week, Monday)
2024-015VALIDOrdinal date (15th day of year)
10:30:00VALIDTime
10:30:00.123VALIDTime with fractional seconds
2024-01-15T10:30:00ZVALIDUTC datetime
2024-01-15T10:30:00+09:00VALIDDatetime with timezone offset
P1Y2M3DT4H5M6SVALIDDuration (1 year, 2 months, 3 days, 4 hours, 5 minutes, 6 seconds)
PT30MVALIDDuration (30 minutes)
2024-13-45INVALIDInvalid: out of range values
Output Format
The parser returns structured types for dates (IsoDate), times (IsoTime), datetimes (IsoDateTime), and durations (IsoDuration), each with properly typed fields.
Structure
IsoDateyear, month, day, week, weekday, ordinal_day fields
IsoTimehour, minute, second, fraction, timezone fields
IsoDateTimedate (IsoDate), time (Option<IsoTime>)
IsoDurationyears, months, days, hours, minutes, seconds fields
Visual Overview
How the Code Works
This section explains how the parsing rules map to the format semantics.
Calendar Date Parsing
.rule(
"calendar_date",
seq(vec![
dynamic(re("[0-9]{4}")),
dynamic(str("-").optional()),
dynamic(re("[0-9]{2}")),
dynamic(str("-").optional()),
dynamic(re("[0-9]{2}")),
]),
)Calendar dates are `YYYY-MM-DD` or `YYYYMMDD`: The separator is optional for compact format.
Week Date Parsing
.rule(
"week_date",
seq(vec![
dynamic(re("[0-9]{4}")),
dynamic(str("-W")),
dynamic(re("[0-9]{2}")), // week (01-53)
dynamic(str("-")),
dynamic(re("[1-7]")), // weekday (1=Monday, 7=Sunday)
]),
)Week dates use `YYYY-Www-D` format: Week 1 contains the year's first Thursday.
Ordinal Date Parsing
.rule(
"ordinal_date",
seq(vec![
dynamic(re("[0-9]{4}")),
dynamic(str("-")),
dynamic(re("[0-9]{3}")), // day of year (001-366)
]),
)Ordinal dates use day of year: Day 015 means January 15th.
Time Parsing
.rule(
"time_basic",
seq(vec![
dynamic(re("[0-9]{2}")),
dynamic(str(":").optional()),
dynamic(re("[0-9]{2}")),
dynamic(str(":").optional()),
dynamic(re("[0-9]{2}")),
dynamic(/* fraction */).optional(),
]),
)
.rule(
"timezone",
choice(vec![
dynamic(str("Z")),
dynamic(/* offset: +HH:MM or -HH:MM */),
]),
)Times include optional fractions and timezone: `Z` indicates UTC; offsets are `+HH:MM` or `-HH:MM`.
Duration Parsing
.rule(
"duration",
seq(vec![
dynamic(str("P")),
dynamic(seq(vec![
dynamic(re("[0-9]+Y").optional()),
dynamic(re("[0-9]+M").optional()),
dynamic(re("[0-9]+D").optional()),
dynamic(seq(vec![
dynamic(str("T")),
dynamic(re("[0-9]+H").optional()),
dynamic(re("[0-9]+M").optional()),
dynamic(re("[0-9]+S").optional()),
]).optional()),
]).optional()),
]),
)Durations use `PnYnMnDTnHnMnS` format: `P` starts date portion, `T` starts time portion.
Date Parsing Implementation
fn parse_date(input: &str) -> Result<IsoDate, String> {
// Week date: YYYY-Www-D
if input.contains("-W") {
let parts: Vec<&str> = input.split("-W").collect();
let year: i32 = parts[0].parse()?;
let week_weekday: Vec<&str> = parts[1].split('-').collect();
let week: u32 = week_weekday[0].parse()?;
let weekday: u32 = week_weekday.get(1).and_then(|s| s.parse().ok()).unwrap_or(1);
return Ok(IsoDate { year, week: Some(week), weekday: Some(weekday), ... });
}
// Ordinal date: YYYY-DDD
if parts.len() == 2 && parts[1].len() == 3 {
return Ok(IsoDate { year, ordinal_day: Some(day_of_year), ... });
}
// Calendar date: YYYY-MM-DD
Ok(IsoDate { year, month: Some(month), day: Some(day), ... })
}Dates are parsed by detecting format:
Date Parsing Implementation
fn parse_date(input: &str) -> Result<IsoDate, String> {
// Week date: YYYY-Www-D
if input.contains("-W") {
let parts: Vec<&str> = input.split("-W").collect();
let year: i32 = parts[0].parse()?;
let week_weekday: Vec<&str> = parts[1].split('-').collect();
let week: u32 = week_weekday[0].parse()?;
let weekday: u32 = week_weekday.get(1).and_then(|s| s.parse().ok()).unwrap_or(1);
return Ok(IsoDate { year, week: Some(week), weekday: Some(weekday), ... });
}
// Ordinal date: YYYY-DDD
if parts.len() == 2 && parts[1].len() == 3 {
return Ok(IsoDate { year, ordinal_day: Some(day_of_year), ... });
}
// Calendar date: YYYY-MM-DD
Ok(IsoDate { year, month: Some(month), day: Some(day), ... })
}Dates are parsed by detecting format:
Source Code
//! ISO 8601 Date/Time Parser Example
//!
//! This example demonstrates parsing ISO 8601 date, time, and datetime strings.
//! ISO 8601 is the international standard for date and time representations.
//!
//! Supported formats:
//! - Calendar dates: 2024-01-15, 20240115
//! - Week dates: 2024-W02-1 (year-week-weekday)
//! - Ordinal dates: 2024-015 (year-day of year)
//! - Times: 10:30:00, 10:30:00.123
//! - Timezones: Z, +02:00, -05:00
//! - Date-times: 2024-01-15T10:30:00Z
//! - Durations: P1Y2M3DT4H5M6S
//!
//! Run with: cargo run --example iso-8601/basic --no-default-features
use parsanol::portable::{
parser_dsl::{choice, dynamic, re, seq, str, GrammarBuilder, ParsletExt},
AstArena, Grammar, PortableParser,
};
use serde::{Deserialize, Serialize};
/// Build ISO 8601 grammar
fn build_iso8601_grammar() -> Grammar {
GrammarBuilder::new()
// Date components
.rule("year", re("[0-9]{4}"))
.rule("month", re("[0-9]{2}"))
.rule("day", re("[0-9]{2}"))
.rule("date_separator", str("-").optional())
// Calendar date: YYYY-MM-DD or YYYYMMDD
.rule(
"calendar_date",
seq(vec![
dynamic(re("[0-9]{4}")),
dynamic(str("-").optional()),
dynamic(re("[0-9]{2}")),
dynamic(str("-").optional()),
dynamic(re("[0-9]{2}")),
]),
)
// Week date: YYYY-Www-D
.rule(
"week_date",
seq(vec![
dynamic(re("[0-9]{4}")),
dynamic(str("-W")),
dynamic(re("[0-9]{2}")), // week
dynamic(str("-")),
dynamic(re("[1-7]")), // weekday (1=Monday, 7=Sunday)
]),
)
// Ordinal date: YYYY-DDD
.rule(
"ordinal_date",
seq(vec![
dynamic(re("[0-9]{4}")),
dynamic(str("-")),
dynamic(re("[0-9]{3}")), // day of year
]),
)
// Time components
.rule("hour", re("[0-9]{2}"))
.rule("minute", re("[0-9]{2}"))
.rule("second", re("[0-9]{2}"))
.rule(
"fraction",
seq(vec![dynamic(str(".")), dynamic(re("[0-9]+"))]),
)
// Time: HH:MM:SS or HHMMSS
.rule(
"time_basic",
seq(vec![
dynamic(re("[0-9]{2}")),
dynamic(str(":").optional()),
dynamic(re("[0-9]{2}")),
dynamic(str(":").optional()),
dynamic(re("[0-9]{2}")),
dynamic(seq(vec![dynamic(str(".")), dynamic(re("[0-9]+"))]).optional()),
]),
)
// Timezone
.rule("utc_designator", str("Z"))
.rule(
"tz_offset",
seq(vec![
dynamic(str("+").or(str("-"))),
dynamic(re("[0-9]{2}")),
dynamic(str(":").optional()),
dynamic(re("[0-9]{2}").optional()),
]),
)
.rule(
"timezone",
choice(vec![
dynamic(str("Z")),
dynamic(seq(vec![
dynamic(str("+").or(str("-"))),
dynamic(re("[0-9]{2}")),
dynamic(str(":").optional()),
dynamic(re("[0-9]{2}").optional()),
])),
]),
)
// Combined date-time
.rule(
"datetime",
seq(vec![
dynamic(choice(vec![
dynamic(re("[0-9]{4}-[0-9]{2}-[0-9]{2}")), // calendar
dynamic(re("[0-9]{4}-W[0-9]{2}-[1-7]")), // week
dynamic(re("[0-9]{4}-[0-9]{3}")), // ordinal
])),
dynamic(str("T").or(str(" "))),
dynamic(re("[0-9]{2}(:[0-9]{2}){1,2}")), // time
dynamic(re("[.,][0-9]+").optional()), // fraction
dynamic(
choice(vec![
dynamic(str("Z")),
dynamic(re("[+-][0-9]{2}(:?[0-9]{2})?")),
])
.optional(),
),
]),
)
// Duration: PnYnMnDTnHnMnS
.rule(
"duration",
seq(vec![
dynamic(str("P")),
dynamic(
seq(vec![
dynamic(re("[0-9]+Y").optional()),
dynamic(re("[0-9]+M").optional()),
dynamic(re("[0-9]+D").optional()),
dynamic(
seq(vec![
dynamic(str("T")),
dynamic(re("[0-9]+H").optional()),
dynamic(re("[0-9]+M").optional()),
dynamic(re("[0-9]+S").optional()),
])
.optional(),
),
])
.optional(),
),
]),
)
// Root: try datetime, then date, then time, then duration
.rule(
"iso_value",
choice(vec![
dynamic(re("[0-9]{4}(-[0-9]{2}){2}[T ][0-9]{2}:[0-9]{2}")), // datetime
dynamic(re("[0-9]{4}-W[0-9]{2}-[1-7]")), // week date
dynamic(re("[0-9]{4}-[0-9]{3}")), // ordinal date
dynamic(re("[0-9]{4}(-[0-9]{2}){0,2}")), // calendar date
dynamic(re("[0-9]{2}:[0-9]{2}(:[0-9]{2})?")), // time
dynamic(re("P[0-9YMDTHMS]+")), // duration
]),
)
.build()
}
/// Parsed ISO 8601 date components
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IsoDate {
pub year: i32,
pub month: Option<u32>,
pub day: Option<u32>,
pub week: Option<u32>,
pub weekday: Option<u32>,
pub ordinal_day: Option<u32>,
}
/// Parsed ISO 8601 time components
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IsoTime {
pub hour: u32,
pub minute: u32,
pub second: u32,
pub fraction: Option<f64>,
pub timezone: Option<String>,
}
/// Parsed ISO 8601 datetime
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IsoDateTime {
pub date: IsoDate,
pub time: Option<IsoTime>,
}
/// Parsed ISO 8601 duration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IsoDuration {
pub years: Option<u32>,
pub months: Option<u32>,
pub days: Option<u32>,
pub hours: Option<u32>,
pub minutes: Option<u32>,
pub seconds: Option<u32>,
}
/// Parse ISO 8601 datetime string
pub fn parse_iso8601(input: &str) -> Result<String, String> {
let grammar = build_iso8601_grammar();
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
let _ast = parser
.parse()
.map_err(|e| format!("Parse error: {:?}", e))?;
// Parse manually for structured output
let input = input.trim();
// Try duration first
if input.starts_with('P') {
let duration = parse_duration(input)?;
return serde_json::to_string_pretty(&duration).map_err(|e| e.to_string());
}
// Try datetime
if let Some(sep_pos) = input.find(['T', ' ']) {
let date_str = &input[..sep_pos];
let time_str = &input[sep_pos + 1..];
let date = parse_date(date_str)?;
let time = parse_time(time_str)?;
let datetime = IsoDateTime {
date,
time: Some(time),
};
return serde_json::to_string_pretty(&datetime).map_err(|e| e.to_string());
}
// Try date only
if input.contains('-') || input.len() == 8 {
let date = parse_date(input)?;
return serde_json::to_string_pretty(&date).map_err(|e| e.to_string());
}
// Try time only
if input.contains(':') {
let time = parse_time(input)?;
return serde_json::to_string_pretty(&time).map_err(|e| e.to_string());
}
Err(format!("Unrecognized ISO 8601 format: {}", input))
}
/// Parse date portion
fn parse_date(input: &str) -> Result<IsoDate, String> {
// Week date: YYYY-Www-D
if input.contains("-W") {
let parts: Vec<&str> = input.split("-W").collect();
if parts.len() != 2 {
return Err("Invalid week date format".into());
}
let year: i32 = parts[0].parse().map_err(|_| "Invalid year")?;
let week_weekday: Vec<&str> = parts[1].split('-').collect();
let week: u32 = week_weekday[0].parse().map_err(|_| "Invalid week")?;
let weekday: u32 = week_weekday
.get(1)
.and_then(|s| s.parse().ok())
.unwrap_or(1);
return Ok(IsoDate {
year,
month: None,
day: None,
week: Some(week),
weekday: Some(weekday),
ordinal_day: None,
});
}
// Ordinal date: YYYY-DDD
let parts: Vec<&str> = input.split('-').collect();
if parts.len() == 2 && parts[1].len() == 3 {
let year: i32 = parts[0].parse().map_err(|_| "Invalid year")?;
let ordinal_day: u32 = parts[1].parse().map_err(|_| "Invalid ordinal day")?;
return Ok(IsoDate {
year,
month: None,
day: None,
week: None,
weekday: None,
ordinal_day: Some(ordinal_day),
});
}
// Calendar date: YYYY-MM-DD or YYYYMMDD
let (year, month, day) = if input.contains('-') {
let parts: Vec<&str> = input.split('-').collect();
let year: i32 = parts[0].parse().map_err(|_| "Invalid year")?;
let month: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(1);
let day: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(1);
(year, month, day)
} else if input.len() == 8 {
let year: i32 = input[0..4].parse().map_err(|_| "Invalid year")?;
let month: u32 = input[4..6].parse().map_err(|_| "Invalid month")?;
let day: u32 = input[6..8].parse().map_err(|_| "Invalid day")?;
(year, month, day)
} else {
return Err(format!("Invalid date format: {}", input));
};
Ok(IsoDate {
year,
month: Some(month),
day: Some(day),
week: None,
weekday: None,
ordinal_day: None,
})
}
/// Parse time portion
fn parse_time(input: &str) -> Result<IsoTime, String> {
// Extract timezone
let (time_part, timezone) = if let Some(pos) = input.rfind(['Z', '+', '-']) {
if input.chars().nth(pos) == Some('Z') {
(&input[..pos], Some("Z".to_string()))
} else if pos > 8 {
// Ensure it's not the date separator
let tz = &input[pos..];
// Validate it looks like a timezone
if tz.starts_with(['+', '-']) {
(&input[..pos], Some(tz.to_string()))
} else {
(input, None)
}
} else {
(input, None)
}
} else {
(input, None)
};
// Parse time components
let parts: Vec<&str> = time_part.split(':').collect();
let hour: u32 = parts[0].parse().map_err(|_| "Invalid hour")?;
let minute: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
// Handle seconds with optional fraction
let (second, fraction) = if let Some(sec_str) = parts.get(2) {
if sec_str.contains('.') {
let sec_parts: Vec<&str> = sec_str.split('.').collect();
let sec: u32 = sec_parts[0].parse().map_err(|_| "Invalid second")?;
let frac: f64 = format!("0.{}", sec_parts[1]).parse().unwrap_or(0.0);
(sec, Some(frac))
} else {
(sec_str.parse().map_err(|_| "Invalid second")?, None)
}
} else {
(0, None)
};
Ok(IsoTime {
hour,
minute,
second,
fraction,
timezone,
})
}
/// Parse duration
fn parse_duration(input: &str) -> Result<IsoDuration, String> {
if !input.starts_with('P') {
return Err("Duration must start with P".into());
}
let rest = &input[1..];
let (date_part, time_part) = if let Some(t_pos) = rest.find('T') {
(&rest[..t_pos], Some(&rest[t_pos + 1..]))
} else {
(rest, None)
};
let mut duration = IsoDuration {
years: None,
months: None,
days: None,
hours: None,
minutes: None,
seconds: None,
};
// Parse date components
let mut num_buf = String::new();
for c in date_part.chars() {
if c.is_ascii_digit() {
num_buf.push(c);
} else {
let value: u32 = num_buf.parse().unwrap_or(0);
num_buf.clear();
match c {
'Y' => duration.years = Some(value),
'M' => duration.months = Some(value),
'D' => duration.days = Some(value),
_ => {}
}
}
}
// Parse time components
if let Some(time) = time_part {
for c in time.chars() {
if c.is_ascii_digit() {
num_buf.push(c);
} else {
let value: u32 = num_buf.parse().unwrap_or(0);
num_buf.clear();
match c {
'H' => duration.hours = Some(value),
'M' => duration.minutes = Some(value),
'S' => duration.seconds = Some(value),
_ => {}
}
}
}
}
Ok(duration)
}
fn main() {
println!("ISO 8601 Date/Time Parser");
println!("=========================\n");
let examples = [
// Calendar dates
("2024-01-15", "Calendar date"),
("20240115", "Compact date"),
// Week dates
("2024-W02-1", "Week date (2nd week, Monday)"),
// Ordinal dates
("2024-015", "Ordinal date (15th day of year)"),
// Times
("10:30:00", "Time"),
("10:30:00.123", "Time with fraction"),
// Date-times
("2024-01-15T10:30:00Z", "UTC datetime"),
("2024-01-15T10:30:00+09:00", "Datetime with timezone"),
// Durations
("P1Y2M3D", "Duration (1 year, 2 months, 3 days)"),
("PT30M", "Duration (30 minutes)"),
("P1Y2M3DT4H5M6S", "Full duration"),
];
for (input, description) in examples {
println!("Input: {} ({})", input, description);
match parse_iso8601(input) {
Ok(json) => println!("Output:\n{}\n", json),
Err(e) => println!("Error: {}\n", e),
}
}
}
# frozen_string_literal: true
# ISO 8601 Date/Time Parser - Ruby Implementation
#
# Parse ISO 8601 dates, times, datetimes, and durations.
#
# Run with: ruby example/iso-8601/basic.rb
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require "parsanol/parslet"
# ISO 8601 parser
class Iso8601Parser < Parsanol::Parser
root :iso_value
# Date components
rule(:year) { match("[0-9]").repeat(4, 4).as(:year) }
rule(:month) { match("[0-9]").repeat(2, 2).as(:month) }
rule(:day) { match("[0-9]").repeat(2, 2).as(:day) }
rule(:date_separator) { str("-").maybe }
# Calendar date: YYYY-MM-DD or YYYYMMDD
rule(:calendar_date) do
year >> date_separator >> month >> date_separator >> day
end
# Week date: YYYY-Www-D
rule(:week_date) do
year >> str("-W") >>
match("[0-9]").repeat(2, 2).as(:week) >>
str("-") >>
match("[1-7]").as(:weekday)
end
# Ordinal date: YYYY-DDD
rule(:ordinal_date) do
year >> str("-") >>
match("[0-9]").repeat(3, 3).as(:ordinal_day)
end
# Time components
rule(:hour) { match("[0-9]").repeat(2, 2).as(:hour) }
rule(:minute) { match("[0-9]").repeat(2, 2).as(:minute) }
rule(:second) { match("[0-9]").repeat(2, 2).as(:second) }
rule(:fraction) { str(".") >> match("[0-9]").repeat(1).as(:fraction) }
rule(:time_separator) { str(":").maybe }
# Time: HH:MM:SS[.frac]
rule(:time_basic) do
hour >> time_separator >> minute >> time_separator >> second >> fraction.maybe
end
# Timezone
rule(:utc_designator) { str("Z").as(:utc) }
rule(:tz_sign) { (str("+") | str("-")).as(:tz_sign) }
rule(:tz_hour) { match("[0-9]").repeat(2, 2).as(:tz_hour) }
rule(:tz_minute) do
(str(":") >> match("[0-9]").repeat(2, 2)).maybe.as(:tz_minute)
end
rule(:tz_offset) do
tz_sign >> tz_hour >> tz_minute
end
rule(:timezone) { utc_designator | tz_offset | str("") }
rule(:time) { time_basic >> timezone }
# Combined date-time
rule(:datetime) do
(calendar_date | week_date | ordinal_date) >>
(str("T") | str(" ")) >>
time
end
# Duration: P[nY][nM][nD][T[nH][nM][nS]]
rule(:duration) do
str("P") >>
(
(match("[0-9]").repeat(1).as(:years) >> str("Y")).maybe >>
(match("[0-9]").repeat(1).as(:months) >> str("M")).maybe >>
(match("[0-9]").repeat(1).as(:days) >> str("D")).maybe >>
(
str("T") >>
(
(match("[0-9]").repeat(1).as(:hours) >> str("H")).maybe >>
(match("[0-9]").repeat(1).as(:minutes) >> str("M")).maybe >>
(match("[0-9]").repeat(1).as(:seconds) >> str("S")).maybe
)
).maybe
)
end
# Top-level alternatives
rule(:iso_value) { datetime | calendar_date | time | duration }
end
# Date result class
IsoDate = Struct.new(:year, :month, :day, :week, :weekday, :ordinal_day) do
def to_s
if week
"#{year}-W#{week}-#{weekday}"
elsif ordinal_day
"#{year}-#{ordinal_day}"
else
"#{year}-#{month}-#{day}"
end
end
def to_date
require "date"
if week
Date.commercial(year.to_i, week.to_i, weekday.to_i)
elsif ordinal_day
Date.ordinal(year.to_i, ordinal_day.to_i)
else
Date.new(year.to_i, month.to_i, day.to_i)
end
end
end
# Time result class
IsoTime = Struct.new(:hour, :minute, :second, :fraction, :utc, :tz_sign,
:tz_hour, :tz_minute) do
def to_s
h = "#{hour}:#{minute}:#{second}"
h += ".#{fraction}" if fraction
h += "Z" if utc
h += "#{tz_sign}#{tz_hour}#{tz_minute}" if tz_hour
h
end
end
# DateTime result class
IsoDateTime = Struct.new(:date, :time) do
def to_s
"#{date}T#{time}"
end
end
# Duration result class
IsoDuration = Struct.new(:years, :months, :days, :hours, :minutes, :seconds) do
def to_s
parts = ["P"]
parts << "#{years}Y" if years
parts << "#{months}M" if months
parts << "#{days}D" if days
time_parts = []
time_parts << "#{hours}H" if hours
time_parts << "#{minutes}M" if minutes
time_parts << "#{seconds}S" if seconds
if time_parts.any?
parts << "T"
parts.concat(time_parts)
end
parts.join
end
def to_seconds
total = 0
total += years.to_i * 365.25 * 24 * 3600 if years
total += months.to_i * 30.44 * 24 * 3600 if months
total += days.to_i * 24 * 3600 if days
total += hours.to_i * 3600 if hours
total += minutes.to_i * 60 if minutes
total += seconds.to_i if seconds
total.to_i
end
end
# Transform parse tree to result objects
class Iso8601Transform < Parsanol::Transform
# Calendar date
rule(year: simple(:y), month: simple(:m), day: simple(:d)) do
IsoDate.new(y.to_s, m.to_s, d.to_s, nil, nil, nil)
end
# Week date
rule(year: simple(:y), week: simple(:w), weekday: simple(:wd)) do
IsoDate.new(y.to_s, nil, nil, w.to_s, wd.to_s, nil)
end
# Ordinal date
rule(year: simple(:y), ordinal_day: simple(:od)) do
IsoDate.new(y.to_s, nil, nil, nil, nil, od.to_s)
end
# Time
rule(hour: simple(:h), minute: simple(:m), second: simple(:s)) do
IsoTime.new(h.to_s, m.to_s, s.to_s, nil, nil, nil, nil)
end
rule(hour: simple(:h), minute: simple(:m), second: simple(:s),
fraction: simple(:f)) do
IsoTime.new(h.to_s, m.to_s, s.to_s, f.to_s, nil, nil, nil)
end
rule(hour: simple(:h), minute: simple(:m), second: simple(:s),
utc: simple(:u)) do
IsoTime.new(h.to_s, m.to_s, s.to_s, nil, u.to_s, nil, nil)
end
rule(hour: simple(:h), minute: simple(:m), second: simple(:s),
tz_sign: simple(:ts), tz_hour: simple(:th)) do
IsoTime.new(h.to_s, m.to_s, s.to_s, nil, nil, ts.to_s, th.to_s, nil)
end
# Duration
rule(
years: simple(:y), months: simple(:mo), days: simple(:d),
hours: simple(:h), minutes: simple(:mi), seconds: simple(:s)
) do
IsoDuration.new(y.to_s, mo.to_s, d.to_s, h.to_s, mi.to_s, s.to_s)
end
end
# Parse ISO 8601 string
def parse_iso8601(str)
parser = Iso8601Parser.new
transform = Iso8601Transform.new
tree = parser.parse(str)
transform.apply(tree)
rescue Parsanol::ParseError => e
puts "Parse error: #{e.message}"
nil
end
# Main demo
if __FILE__ == $PROGRAM_NAME
puts "ISO 8601 Date/Time Parser"
puts "=" * 50
puts
examples = [
# Calendar dates
["2024-01-15", "Calendar date"],
["20240115", "Compact date"],
["2024-12-25", "Christmas"],
# Week dates
["2024-W02-1", "Week date (2nd week, Monday)"],
# Ordinal dates
["2024-015", "Ordinal date (15th day)"],
# Times
["10:30:00", "Time"],
["10:30:00.123", "Time with fraction"],
["10:30:00Z", "UTC time"],
["10:30:00+09:00", "Time with timezone"],
# Date-times
["2024-01-15T10:30:00Z", "DateTime UTC"],
["2024-01-15T10:30:00+09:00", "DateTime with timezone"],
# Durations
["P1Y2M3DT4H5M6S", "Full duration"],
["PT30M", "30 minutes duration"],
["P1D", "1 day duration"],
]
examples.each do |input, description|
puts "#{description}:"
puts " Input: #{input}"
result = parse_iso8601(input)
if result
puts " Result: #{result.inspect}"
puts " String: #{result}"
if result.respond_to?(:to_date)
puts " Date: #{begin
result.to_date
rescue StandardError
'N/A'
end}"
end
puts " Seconds: #{result.to_seconds}" if result.respond_to?(:to_seconds)
end
puts
end
end