use std::borrow::Cow;
use std::collections::BTreeMap;
use std::env;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::path::PathBuf;
use parse_zoneinfo::line::Line;
use parse_zoneinfo::table::{Table, TableBuilder};
use parse_zoneinfo::transitions::{
FixedTimespan,
FixedTimespanSet,
TableTransitions
};
fn strip_comments(mut line: String) -> String {
if let Some(pos) = line.find('#') {
line.truncate(pos);
};
line
}
fn tz_identifier(name: &str) -> String {
let name = name.replace('/', "__").replace('+', "Plus");
if let Some(pos) = name.find('-') {
if name[pos + 1..].chars().next().map(char::is_numeric).unwrap_or(false) {
name.replace('-', "Minus")
} else {
name.replace('-', "")
}
} else {
name
}
}
fn offset_short_name(name: &'_ str, offset: i64) -> Cow<'_, str> {
if name != "%z" {
return Cow::Borrowed(name)
}
let sign = if offset.is_negative() {"-"} else {"+"};
let off = offset.abs();
let minutes = off / 60;
let secs = (off % 60) as u8;
let mins = (minutes % 60) as u8;
let hours = (minutes / 60) as u8;
assert!(
secs == 0,
"numeric names are not used if the offset has fractional minutes"
);
if mins != 0 {
Cow::Owned(format!("{sign}{hours:02}{mins:02}"))
}
else {
Cow::Owned(format!("{sign}{hours:02}"))
}
}
struct Definitions {
names: Vec<String>,
zones: Vec<String>,
offsets: Vec<String>
}
impl Definitions {
fn collect(table: &Table) -> Self {
enum Entry {
Zone,
Link(String)
}
let zones = table.zonesets
.keys()
.map(|name| (name.clone(), Entry::Zone));
let links = table.links
.iter()
.map(|(name, target)| (name.clone(), Entry::Link(target.clone())));
let entries: BTreeMap<String, Entry> = zones.chain(links).collect();
let mut names = Vec::new();
let mut zones = Vec::new();
let mut offsets = Vec::new();
for (name, entry) in entries {
names.push(name.clone());
let set = table.timespans(&name).unwrap();
match entry {
Entry::Zone => {
zones.push(Self::define_zone(&name, &set));
if set.rest.is_empty() {
offsets.push(Self::define_offset(&name, &set.first));
}
},
Entry::Link(target) => {
zones.push(Self::define_link(&name, &target));
if set.rest.is_empty() {
offsets.push(Self::define_offset_link(&name, &target));
}
}
}
}
Self {names, zones, offsets}
}
fn define_offset(name: &str, span: &FixedTimespan) -> String {
let ident = tz_identifier(name);
let offset = span.utc_offset + span.dst_offset;
let short_name = offset_short_name(&span.name, offset);
format!(
"\t/// {name}\n\
\tpub const {ident}: Self = Self::new({short_name:?}, {offset});\n"
)
}
fn define_offset_link(name: &str, target: &str) -> String {
let ident = tz_identifier(name);
let target_ident = tz_identifier(target);
let doc = format!("/// {name} (an alias for {target})");
format!("\t{doc}\n\tpub const {ident}: Self = Self::{target_ident};\n")
}
fn define_zone(name: &str, set: &FixedTimespanSet) -> String {
let ident = tz_identifier(name);
let doc = format!("/// {name}");
let decl = format!("pub const {ident}: Self = Self::define(");
let lmt_offset = set.first.utc_offset + set.first.dst_offset;
let lmt_short_name = offset_short_name(&set.first.name, lmt_offset);
let lmt = format!("Offset::new({lmt_short_name:?}, {lmt_offset})");
let offsets = Self::make_offsets(&set.rest);
format!(
"\t{doc}\n\
\t{decl}\n\
\t\t{name:?},\n\
\t\t{lmt},\n\
\t\t{offsets}\n\
\t);\n"
)
}
fn define_link(name: &str, target: &str) -> String {
let ident = tz_identifier(name);
let target_ident = tz_identifier(target);
format!(
"\t/// {name} (an alias for {target})\n\
\tpub const {ident}: Self = Self::alias(\n\
\t\t{name:?},\n\
\t\tSelf::{target_ident}\n\
\t);\n"
)
}
fn make_offsets(offsets: &[(i64, FixedTimespan)]) -> String {
if offsets.is_empty() {
return String::from("&[]");
}
let mut res = String::from("&[");
use std::fmt::Write;
for (p, FixedTimespan { utc_offset, dst_offset, name }) in offsets {
let seconds = utc_offset + dst_offset;
let short_name = offset_short_name(name, seconds);
write!(
res,
"\n\t\t\t{},",
format_args!("(Point{{timestamp: {p}}}, \
Offset::new({short_name:?}, {seconds}))"
)
).unwrap();
}
write!(res, "\n\t\t]").unwrap();
res
}
fn write_file(self, file: &mut File) -> io::Result<()> {
if !self.zones.is_empty() {
writeln!(file, "#[allow(non_upper_case_globals)]")?;
writeln!(file, "impl Zone {{")?;
for def in self.zones {
file.write_all(def.as_bytes())?;
}
writeln!(file, "}}")?;
}
if !self.offsets.is_empty() {
writeln!(file, "#[allow(non_upper_case_globals)]")?;
writeln!(file, "impl Offset {{")?;
for def in self.offsets {
file.write_all(def.as_bytes())?;
}
writeln!(file, "}}")?;
}
if !self.names.is_empty() {
writeln!(
file,
"/// Static map of all [`Zone`]s and their names, \
generated at compile time"
)?;
writeln!(
file,
"pub static ZONES: phf::Map<&'static str, Zone> = \
phf::phf_map! {{"
)?;
for name in self.names {
writeln!(
file,
"\t{name:?} => Zone::{},",
tz_identifier(&name)
)?;
}
writeln!(file, "}};")?;
}
Ok(())
}
}
fn main() {
let mut table = TableBuilder::new();
let tzfiles = [
"tz/africa",
"tz/antarctica",
"tz/asia",
"tz/australasia",
"tz/backward",
"tz/etcetera",
"tz/europe",
"tz/northamerica",
"tz/southamerica",
];
let manifest_dir = match env::var("CARGO_MANIFEST_DIR") {
Ok(p) => PathBuf::from(p),
Err(_) => PathBuf::new()
};
let lines = tzfiles
.iter()
.map(|p| manifest_dir.join(p))
.map(|path| File::open(&path)
.map_err(|e| format!("cannot open {}: {e}", path.display()))
.unwrap()
)
.map(BufReader::new)
.flat_map(BufRead::lines)
.map(Result::unwrap)
.map(strip_comments);
for line in lines {
match Line::new(&line).unwrap() {
Line::Zone(zone) => table.add_zone_line(zone).unwrap(),
Line::Continuation(cont) => table.add_continuation_line(cont).unwrap(),
Line::Rule(rule) => table.add_rule_line(rule).unwrap(),
Line::Link(link) => table.add_link_line(link).unwrap(),
Line::Space => {}
}
}
let table = table.build();
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let timezone_path = out_dir.join("timezones.rs");
let mut timezone_file = File::create(timezone_path).unwrap();
Definitions::collect(&table).write_file(&mut timezone_file).unwrap();
println!("cargo:rerun-if-changed=build.rs");
}