blob: 2a47c5b423ed9de5cb7981514180c363cd5bd8c8 [file] [log] [blame] [edit]
//! Determining the structure of a set of ruleset names.
//!
//! The names of time zones in the zoneinfo database are of the form
//! `Area/Location`, or more rarely, `Area/Location/Sublocation`. This means
//! they form a hierarchy, with each level either serving as a time zone
//! itself (usually a location) or as a parent of multiple other entries
//! (usually an area).
//!
//! When generating Rust code containing the timezone data, we need to
//! generate the entire tree structure, not just the leaves of actual timezone
//! data. This module determines that structure, allowing it to be created
//! before any actual timezone data is written.
//!
//! For example, say we have the following subset of time zone entries:
//!
//! - America/Antigua
//! - America/Araguaina
//! - America/Argentina/Buenos_Aires
//! - America/Argentina/Catamarca
//! - America/Argentina/Cordoba
//! - America/Aruba
//!
//! On top of the six actual time zone files, we would need to create the following:
//!
//! - An America module that has three private submodules (Antigua, Araguaína,
//! and Aruba) and one public submodule (Argentina);
//! - An America/Argentina submodule that has there private submodules (Buenos
//! Aires, Catamarca, Cordoba).
//!
//! This module contains an iterator that finds all parent zonesets, and
//! sorts them so they’re output in a correct order.
use std::collections::{BTreeMap, BTreeSet};
use crate::table::Table;
/// Trait to put the `structure` method on Tables.
pub trait Structure {
/// Returns an iterator over the structure of this table.
fn structure(&self) -> TableStructure;
}
impl Structure for Table {
fn structure(&self) -> TableStructure {
let mut mappings = BTreeMap::new();
for key in self.zonesets.keys().chain(self.links.keys()) {
// Extract the name from the *last* slash. So
// `America/Kentucky/Louisville` is split into
// `America/Kentucky` and `Louisville` components.
let last_slash = match key.rfind('/') {
Some(pos) => pos,
None => continue,
};
// Split the string around the slash, which gets removed.
let parent = &key[..last_slash];
{
let set = mappings.entry(parent).or_insert_with(BTreeSet::new);
set.insert(Child::TimeZone(&key[last_slash + 1..]));
}
// If the *parent* name still has a slash in it, then this is
// a time zone of the form `America/Kentucky/Louisville`. We
// need to make sure that `America` now has a `Kentucky`
// child, too.
if let Some(first_slash) = parent.find('/') {
let grandparent = &parent[..first_slash];
let set = mappings.entry(grandparent).or_insert_with(BTreeSet::new);
set.insert(Child::Submodule(&parent[first_slash + 1..]));
}
}
TableStructure { mappings }
}
}
/// The structure of a set of time zone names.
#[derive(PartialEq, Debug)]
pub struct TableStructure<'table> {
mappings: BTreeMap<&'table str, BTreeSet<Child<'table>>>,
}
impl<'table> IntoIterator for TableStructure<'table> {
type Item = TableStructureEntry<'table>;
type IntoIter = Iter<'table>;
fn into_iter(self) -> Self::IntoIter {
// It’s necessary to sort the keys before producing them, to
// ensure that (for example) `America` is produced before
// `America/Kentucky`.
let mut keys: Vec<_> = self.mappings.keys().cloned().collect();
keys.sort_by(|a, b| b.cmp(a));
Iter {
structure: self,
keys,
}
}
}
/// Iterator over sorted entries in a `TableStructure`.
#[derive(PartialEq, Debug)]
pub struct Iter<'table> {
structure: TableStructure<'table>,
keys: Vec<&'table str>,
}
impl<'table> Iterator for Iter<'table> {
type Item = TableStructureEntry<'table>;
fn next(&mut self) -> Option<Self::Item> {
let key = self.keys.pop()?;
// Move the strings out into an (automatically-sorted) vector.
let values = self.structure.mappings[key].iter().cloned().collect();
Some(TableStructureEntry {
name: key,
children: values,
})
}
}
/// An entry returned from a `TableStructure` iterator.
#[derive(PartialEq, Debug)]
pub struct TableStructureEntry<'table> {
/// This entry’s name, which *can* still include slashes.
pub name: &'table str,
/// A vector of sorted child names, which should have no slashes in.
pub children: Vec<Child<'table>>,
}
/// A child module that needs to be created.
///
/// The order here is important for `PartialOrd`: submodules need to be
/// created before actual time zones, as directories need to be created
/// before the files in them can be written.
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Copy, Clone)]
pub enum Child<'table> {
/// A module containing **only** submodules, no time zones.
Submodule(&'table str),
/// A module containing **only** the details of a time zone.
TimeZone(&'table str),
}
#[cfg(test)]
#[allow(unused_results)]
mod test {
use super::*;
use crate::table::Table;
#[test]
fn empty() {
let table = Table::default();
let mut structure = table.structure().into_iter();
assert_eq!(structure.next(), None);
}
#[test]
fn separate() {
let mut table = Table::default();
table.zonesets.insert("a".to_owned(), Vec::new());
table.zonesets.insert("b".to_owned(), Vec::new());
table.zonesets.insert("c".to_owned(), Vec::new());
let mut structure = table.structure().into_iter();
assert_eq!(structure.next(), None);
}
#[test]
fn child() {
let mut table = Table::default();
table.zonesets.insert("a/b".to_owned(), Vec::new());
let mut structure = table.structure().into_iter();
assert_eq!(
structure.next(),
Some(TableStructureEntry {
name: "a",
children: vec![Child::TimeZone("b")]
})
);
assert_eq!(structure.next(), None);
}
#[test]
fn hierarchy() {
let mut table = Table::default();
table.zonesets.insert("a/b/c".to_owned(), Vec::new());
table.zonesets.insert("a/b/d".to_owned(), Vec::new());
table.zonesets.insert("a/e".to_owned(), Vec::new());
let mut structure = table.structure().into_iter();
assert_eq!(
structure.next(),
Some(TableStructureEntry {
name: "a",
children: vec![Child::Submodule("b"), Child::TimeZone("e")]
})
);
assert_eq!(
structure.next(),
Some(TableStructureEntry {
name: "a/b",
children: vec![Child::TimeZone("c"), Child::TimeZone("d")]
})
);
assert_eq!(structure.next(), None);
}
}