use std::collections::{BTreeSet, HashMap, HashSet};
fn doselect(_o: &Options, h: &mut dyn Host, items: &[String]) -> Option<String> {
let width = items.len().to_string().len();
let mut relist = true;
loop {
if relist {
for (i, it) in items.iter().enumerate() {
h.err(&format!("{:>width$}) {}\n", i + 1, it, width = width));
}
relist = false;
}
h.err("#? ");
let line = match h.read_line() {
Some(l) => l,
None => {
h.out("\n");
return None;
}
};
if line.is_empty() {
relist = true;
continue;
}
if line.bytes().all(|b| b.is_ascii_digit()) {
if let Ok(n) = line.parse::<usize>() {
if (1..=items.len()).contains(&n) {
return Some(items[n - 1].clone());
}
}
}
h.err("Please enter a number in range.\n");
}
}
fn ask_continent(o: &Options, h: &mut dyn Host, zonetab: &str) -> Option<String> {
h.err("Please select a continent, ocean, \"coord\", \"TZ\", \"time\", or \"now\".\n");
let mut items: Vec<String> = continents(zonetab);
items.push("coord - I want to use geographical coordinates.".to_string());
items.push("TZ - I want to specify the timezone using a proleptic TZ string.".to_string());
items.push("time - I know local time already.".to_string());
items.push("now - Like \"time\", but configure only for timestamps from now on.".to_string());
let sel = doselect(o, h, &items)?;
let cont = if sel == "Americas" {
"America".to_string()
} else {
sel.split(' ').next().unwrap_or("").to_string()
};
Some(cont)
}
fn continents(zonetab: &str) -> Vec<String> {
let mut set: BTreeSet<String> = BTreeSet::new();
for line in zonetab.lines() {
if line.starts_with("#@") {
let f: Vec<&str> = line.split('\t').collect();
for c in f.get(1).copied().unwrap_or("").split(',') {
handle_entry(c, &mut set);
}
} else if !line.starts_with('#') && !line.is_empty() {
let f: Vec<&str> = line.split('\t').collect();
handle_entry(f.get(2).copied().unwrap_or(""), &mut set);
}
}
set.into_iter().collect()
}
fn handle_entry(entry: &str, set: &mut BTreeSet<String>) {
let mut e = match entry.find('/') {
Some(i) => entry[..i].to_string(),
None => String::new(), };
if e == "America" {
e = "Americas".to_string();
}
if matches!(e.as_str(), "Arctic" | "Atlantic" | "Indian" | "Pacific") {
e.push_str(" Ocean");
}
set.insert(e);
}
fn country_menu(o: &Options, h: &mut dyn Host, continent_re: &str, country_table: &str, zone_table: &str) -> Vec<String> {
let _ = (o, &h);
let mut names = output_country_list(continent_re, country_table, zone_table);
sort_fold(&mut names);
cmd_subst(names)
}
fn output_country_list(continent_re: &str, country_table: &str, zone_table: &str) -> Vec<String> {
let mut cc_list: Vec<String> = Vec::new();
let mut cc_seen: HashSet<String> = HashSet::new();
let mut cc_elsewhere: HashSet<String> = HashSet::new();
for line in zone_table.lines() {
let commentary = line.starts_with("#@");
let f: Vec<&str> = line.split('\t').collect();
let (col1ccs, conts) = if commentary {
let c1 = f.first().copied().unwrap_or("");
let c1 = if c1.len() >= 2 { &c1[2..] } else { "" };
(c1.to_string(), f.get(1).copied().unwrap_or("").to_string())
} else {
(
f.first().copied().unwrap_or("").to_string(),
f.get(2).copied().unwrap_or("").to_string(),
)
};
let cc: Vec<&str> = col1ccs.split(',').collect();
let cont: Vec<&str> = conts.split(',').collect();
for code in &cc {
let mut elsewhere = commentary;
for c in &cont {
if continent_matches(c, continent_re) {
if cc_seen.insert(code.to_string()) {
cc_list.push(code.to_string());
}
elsewhere = false;
}
}
if elsewhere {
for c2 in &cc {
cc_elsewhere.insert(c2.to_string());
}
}
}
}
let mut cc_name: HashMap<String, String> = HashMap::new();
for line in country_table.lines() {
if !line.starts_with('#') {
let f: Vec<&str> = line.split('\t').collect();
if let (Some(code), Some(name)) = (f.first(), f.get(1)) {
cc_name.insert(code.to_string(), name.to_string());
}
}
}
let mut out = Vec::new();
for cc in &cc_list {
if cc_elsewhere.contains(cc) {
continue;
}
out.push(cc_name.get(cc).cloned().unwrap_or_else(|| cc.clone()));
}
out
}
fn continent_matches(cont: &str, continent_re: &str) -> bool {
let pat = continent_re.strip_prefix('^').unwrap_or(continent_re);
cont.starts_with(pat)
}
fn cc_matches(field1: &str, cc: &str) -> bool {
if cc.is_empty() {
return true; }
field1.contains(cc)
}
fn pick_country(o: &Options, h: &mut dyn Host, countries: &[String]) -> Option<(Option<String>, String)> {
match countries.len() {
0 => Some((None, String::new())),
1 => Some((None, countries[0].clone())),
_ => {
h.err("Please select a country whose clocks agree with yours.\n");
let sel = doselect(o, h, countries)?;
Some((Some(sel.clone()), sel))
}
}
}
fn regions_for_country(country: &str, country_table: &str, zone_table: &str) -> Vec<String> {
let cc = country_to_cc(country, country_table);
let mut out = Vec::new();
for line in zone_table.lines() {
if line.starts_with('#') {
continue;
}
let f: Vec<&str> = line.split('\t').collect();
if cc_matches(f.first().copied().unwrap_or(""), &cc) {
out.push(f.get(3).copied().unwrap_or("").to_string());
}
}
cmd_subst(out)
}
fn derive_tz(country: &str, region: &str, country_table: &str, zone_table: &str) -> String {
let cc = country_to_cc(country, country_table);
let mut out = Vec::new();
for line in zone_table.lines() {
if line.starts_with('#') {
continue;
}
let f: Vec<&str> = line.split('\t').collect();
let matches_region = f.get(3).copied().unwrap_or("") == region || region.is_empty();
if cc_matches(f.first().copied().unwrap_or(""), &cc) && matches_region {
out.push(f.get(2).copied().unwrap_or("").to_string());
}
}
cmd_subst(out).join("\n")
}
fn country_to_cc(country: &str, country_table: &str) -> String {
for line in country_table.lines() {
if !line.starts_with('#') {
let f: Vec<&str> = line.split('\t').collect();
if f.get(1).copied() == Some(country) {
return f.first().copied().unwrap_or(country).to_string();
}
}
}
country.to_string()
}
fn sort_fold(v: &mut [String]) {
v.sort_by(|a, b| {
let fa: Vec<u8> = a.bytes().map(|x| x.to_ascii_lowercase()).collect();
let fb: Vec<u8> = b.bytes().map(|x| x.to_ascii_lowercase()).collect();
fa.cmp(&fb).then_with(|| a.as_bytes().cmp(b.as_bytes()))
});
}
fn cmd_subst(mut lines: Vec<String>) -> Vec<String> {
while lines.last().map(|s| s.is_empty()).unwrap_or(false) {
lines.pop();
}
lines
}
fn output_distances(coord: &str, country_table: &str, zone_table: &str) -> Vec<(f64, String)> {
let mut country: HashMap<String, String> = HashMap::new();
for line in country_table.lines() {
if line.starts_with('#') {
continue;
}
let f: Vec<&str> = line.split('\t').collect();
if let (Some(code), Some(name)) = (f.first(), f.get(1)) {
country.insert(code.to_string(), name.to_string());
}
}
country.insert("US".to_string(), "US".to_string());
let mut rows: Vec<Vec<String>> = Vec::new();
let mut cc_used: HashMap<String, u32> = HashMap::new();
for line in zone_table.lines() {
if line.starts_with('#') {
continue;
}
let f: Vec<String> = line.split('\t').map(|s| s.to_string()).collect();
for c in f.first().map(|s| s.as_str()).unwrap_or("").split(',') {
*cc_used.entry(c.to_string()).or_insert(0) += 1;
}
rows.push(f);
}
let (coord_lat, coord_long) = (convert_latitude(coord), convert_longitude(coord));
let mut result = Vec::new();
for f in &rows {
let f1 = f.first().map(|s| s.as_str()).unwrap_or("");
let f2 = f.get(1).map(|s| s.as_str()).unwrap_or("");
let f3 = f.get(2).map(|s| s.as_str()).unwrap_or("");
let f4 = f.get(3).map(|s| s.as_str()).unwrap_or("");
let mut outline = format!("{f1}\t{f2}\t{f3}");
let mut sep = "\t";
let mut item_seen: HashSet<String> = HashSet::new();
item_seen.insert(String::new());
for c in f1.split(',') {
let item = if *cc_used.get(c).unwrap_or(&0) <= 1 {
country.get(c).cloned().unwrap_or_default()
} else {
f4.to_string()
};
if !item_seen.insert(item.clone()) {
continue;
}
outline.push_str(sep);
outline.push_str(&item);
sep = "; ";
}
let here_lat = convert_latitude(f2);
let here_long = convert_longitude(f2);
let d = dist(coord_lat, coord_long, here_lat, here_long);
result.push((d, outline));
}
result
}
fn convert_latitude(coord: &str) -> f64 {
let (lat, _) = split_coord(coord);
convert_coord(lat)
}
fn convert_longitude(coord: &str) -> f64 {
let (_, long) = split_coord(coord);
convert_coord(long)
}
fn split_coord(coord: &str) -> (&str, &str) {
let b = coord.as_bytes();
let mut last = None;
for (i, &c) in b.iter().enumerate().skip(1) {
if c == b'-' || c == b'+' {
last = Some(i);
}
}
match last {
Some(i) => (&coord[..i], &coord[i..]),
None => ("", coord),
}
}
const DEG_TO_RAD: f64 = 0.017453292519943296;
fn convert_coord(coord: &str) -> f64 {
let n = awk_numf(coord);
let digits = leading_digit_count(coord);
let deg = if digits == 6 || digits == 7 {
let degminsec = n;
let intdeg = trunc_div(degminsec, 10000.0);
let minsec = degminsec - intdeg * 10000.0;
let intmin = trunc_div(minsec, 100.0);
let sec = minsec - intmin * 100.0;
(intdeg * 3600.0 + intmin * 60.0 + sec) / 3600.0
} else if digits == 4 || digits == 5 {
let degmin = n;
let intdeg = trunc_div(degmin, 100.0);
let minute = degmin - intdeg * 100.0;
(intdeg * 60.0 + minute) / 60.0
} else {
n
};
deg * DEG_TO_RAD
}
fn trunc_div(x: f64, d: f64) -> f64 {
if x < 0.0 {
-((-x / d).trunc())
} else {
(x / d).trunc()
}
}
fn leading_digit_count(coord: &str) -> usize {
let b = coord.as_bytes();
let mut i = 0;
if i < b.len() && (b[i] == b'-' || b[i] == b'+') {
i += 1;
}
let mut n = 0;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
n += 1;
}
n
}
fn dist(lat1: f64, long1: f64, lat2: f64, long2: f64) -> f64 {
gcdist(lat1, long1, lat2, long2) + pardist(lat1, long1, lat2, long2)
}
fn gcdist(lat1: f64, long1: f64, lat2: f64, long2: f64) -> f64 {
let dlong = long2 - long1;
let x = lat2.cos() * dlong.sin();
let y = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * dlong.cos();
let num = (x * x + y * y).sqrt();
let denom = lat1.sin() * lat2.sin() + lat1.cos() * lat2.cos() * dlong.cos();
num.atan2(denom)
}
fn pardist(lat1: f64, long1: f64, lat2: f64, long2: f64) -> f64 {
(long1 - long2).abs() * lat1.cos().min(lat2.cos())
}
fn build_time_table(h: &mut dyn Host, zone_table: &str) -> Vec<String> {
let outlines = time_outlines(zone_table);
let mut table = Vec::new();
for (h_idx, (tz, outline)) in outlines.iter().enumerate() {
let datestr = h
.run_date_fmt(tz, "%Y %m %d %H:%M %a %b")
.unwrap_or_default();
table.push(format!("{h_idx} {datestr}\t{outline}"));
}
table
}
fn time_outlines(zone_table: &str) -> Vec<(String, String)> {
let mut rows: Vec<Vec<String>> = Vec::new();
let mut cc_used: HashMap<String, u32> = HashMap::new();
for line in zone_table.lines() {
if line.starts_with('#') {
continue;
}
let f: Vec<String> = line.split('\t').map(|s| s.to_string()).collect();
for c in f.first().map(|s| s.as_str()).unwrap_or("").split(',') {
*cc_used.entry(c.to_string()).or_insert(0) += 1;
}
rows.push(f);
}
let mut out = Vec::new();
for f in &rows {
let f1 = f.first().map(|s| s.as_str()).unwrap_or("");
let f2 = f.get(1).map(|s| s.as_str()).unwrap_or("");
let f3 = f.get(2).map(|s| s.as_str()).unwrap_or("");
let f4 = f.get(3).map(|s| s.as_str()).unwrap_or("");
let mut outline = format!("{f1}\t{f2}\t{f3}");
let mut sep = "\t";
let mut item_seen: HashSet<String> = HashSet::new();
item_seen.insert(String::new());
for c in f1.split(',') {
let item = if *cc_used.get(c).unwrap_or(&0) <= 1 {
String::new()
} else {
f4.to_string()
};
if !item_seen.insert(item.clone()) {
continue;
}
outline.push_str(sep);
outline.push_str(&item);
sep = "; ";
}
out.push((f3.to_string(), outline));
}
out
}
fn time_key(line: &str) -> String {
let f: Vec<&str> = line.split_whitespace().collect();
let g = |i: usize| f.get(i).copied().unwrap_or("");
format!("{} {} {} {}", g(5), g(6), g(3), g(4))
}
fn sort_time_table(table: &[String]) -> Vec<String> {
let key = |l: &String| -> (f64, String, f64) {
let f: Vec<&str> = l.split_whitespace().collect();
let n = |i: usize| f.get(i).and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0);
let k25 = f.get(1..5).map(|s| s.join(" ")).unwrap_or_default();
(n(1), k25, n(0))
};
let mut v: Vec<String> = table.to_vec();
v.sort_by(|a, b| {
let (a1, a2, a3) = key(a);
let (b1, b2, b3) = key(b);
a1.partial_cmp(&b1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a2.cmp(&b2))
.then(a3.partial_cmp(&b3).unwrap_or(std::cmp::Ordering::Equal))
});
v
}
fn secs_match(tzdate: &str, utdate: &str) -> bool {
secs_of(tzdate) == secs_of(utdate)
}
fn secs_of(d: &str) -> String {
let b = d.as_bytes();
let mut cut = 0;
let mut i = 0;
while i + 2 < b.len() {
if (b'0'..=b'5').contains(&b[i]) && b[i + 1].is_ascii_digit() && b[i + 2] == b':' {
cut = i + 3;
}
i += 1;
}
let rest = &d[cut..];
rest.bytes()
.take_while(|c| c.is_ascii_digit())
.map(|c| c as char)
.collect()
}
pub fn posix_tz_valid(tz: &str) -> bool {
if tz.starts_with(':') {
return true; }
let b = tz.as_bytes();
let mut p = 0;
if !match_tzname(b, &mut p) {
return false;
}
if !match_offset(b, &mut p) {
return false;
}
let save = p;
if match_tzname(b, &mut p) {
let _ = match_offset(b, &mut p);
let s2 = p;
if !(match_datetime(b, &mut p) && match_datetime(b, &mut p)) {
p = s2;
}
} else {
p = save;
}
p == b.len()
}
fn is_alpha(c: u8) -> bool {
c.is_ascii_alphabetic()
}
fn is_alnum_pm(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'+' || c == b'-'
}
fn match_tzname(b: &[u8], p: &mut usize) -> bool {
let start = *p;
if b.get(*p) == Some(&b'<') {
let mut q = *p + 1;
let mut n = 0;
while q < b.len() && is_alnum_pm(b[q]) {
q += 1;
n += 1;
}
if n >= 3 && b.get(q) == Some(&b'>') {
*p = q + 1;
return true;
}
*p = start;
return false;
}
let mut q = *p;
let mut n = 0;
while q < b.len() && is_alpha(b[q]) {
q += 1;
n += 1;
}
if n >= 3 {
*p = q;
true
} else {
*p = start;
false
}
}
fn match_hhmm(b: &[u8], p: &mut usize) {
let consume = |b: &[u8], p: &mut usize| -> bool {
if b.get(*p) == Some(&b':')
&& b.get(*p + 1).map(|c| (b'0'..=b'5').contains(c)).unwrap_or(false)
&& b.get(*p + 2).map(|c| c.is_ascii_digit()).unwrap_or(false)
{
*p += 3;
true
} else {
false
}
};
if consume(b, p) {
let _ = consume(b, p);
}
}
fn match_offset(b: &[u8], p: &mut usize) -> bool {
let start = *p;
if matches!(b.get(*p), Some(b'-') | Some(b'+')) {
*p += 1;
}
if !match_hour_0_24(b, p) {
*p = start;
return false;
}
match_hhmm(b, p);
true
}
fn match_hour_0_24(b: &[u8], p: &mut usize) -> bool {
if b.get(*p) == Some(&b'2') && b.get(*p + 1).map(|c| (b'0'..=b'4').contains(c)).unwrap_or(false) {
*p += 2;
return true;
}
let mut q = *p;
if matches!(b.get(q), Some(b'0') | Some(b'1')) && b.get(q + 1).map(|c| c.is_ascii_digit()).unwrap_or(false) {
q += 1; }
if b.get(q).map(|c| c.is_ascii_digit()).unwrap_or(false) {
*p = q + 1;
true
} else {
false
}
}
fn match_time(b: &[u8], p: &mut usize) -> bool {
let start = *p;
if matches!(b.get(*p), Some(b'-') | Some(b'+')) {
*p += 1;
}
if !match_hour_0_167(b, p) {
*p = start;
return false;
}
match_hhmm(b, p);
true
}
fn match_hour_0_167(b: &[u8], p: &mut usize) -> bool {
if b.get(*p) == Some(&b'1')
&& b.get(*p + 1) == Some(&b'6')
&& b.get(*p + 2).map(|c| (b'0'..=b'7').contains(c)).unwrap_or(false)
{
*p += 3;
return true;
}
let mut q = *p;
if b.get(q) == Some(&b'1') && b.get(q + 1).map(|c| (b'0'..=b'5').contains(c)).unwrap_or(false) {
q += 1;
} else if b.get(q).map(|c| c.is_ascii_digit()).unwrap_or(false)
&& b.get(q + 1).map(|c| c.is_ascii_digit()).unwrap_or(false)
{
q += 1; }
if b.get(q).map(|c| c.is_ascii_digit()).unwrap_or(false) {
*p = q + 1;
true
} else {
false
}
}
fn match_datetime(b: &[u8], p: &mut usize) -> bool {
let start = *p;
if b.get(*p) != Some(&b',') {
return false;
}
*p += 1;
if !(match_mdate(b, p) || match_jdate(b, p)) {
*p = start;
return false;
}
if b.get(*p) == Some(&b'/') {
let s = *p;
*p += 1;
if !match_time(b, p) {
*p = s; }
}
true
}
fn match_mdate(b: &[u8], p: &mut usize) -> bool {
let start = *p;
if b.get(*p) != Some(&b'M') {
return false;
}
let mut q = *p + 1;
if b.get(q) == Some(&b'1') && b.get(q + 1).map(|c| (b'0'..=b'2').contains(c)).unwrap_or(false) {
q += 2;
} else if b.get(q).map(|c| (b'1'..=b'9').contains(c)).unwrap_or(false) {
q += 1;
} else {
return false;
}
if b.get(q) != Some(&b'.') {
return false;
}
q += 1;
if !b.get(q).map(|c| (b'1'..=b'5').contains(c)).unwrap_or(false) {
return false;
}
q += 1;
if b.get(q) != Some(&b'.') {
return false;
}
q += 1;
if !b.get(q).map(|c| (b'0'..=b'6').contains(c)).unwrap_or(false) {
*p = start;
return false;
}
q += 1;
*p = q;
true
}
fn match_jdate(b: &[u8], p: &mut usize) -> bool {
let start = *p;
let has_j = b.get(*p) == Some(&b'J');
let mut q = *p + usize::from(has_j);
let d0 = b.get(q).copied();
if !d0.map(|c| c.is_ascii_digit()).unwrap_or(false) {
return false;
}
let mut digs = Vec::new();
while digs.len() < 3 && b.get(q).map(|c| c.is_ascii_digit()).unwrap_or(false) {
digs.push(b[q]);
q += 1;
}
let val: i32 = std::str::from_utf8(&digs).unwrap().parse().unwrap();
let ok = if has_j {
(1..=365).contains(&val)
} else {
(0..=365).contains(&val)
};
let leading_zero_multi = digs.len() > 1 && digs[0] == b'0';
if ok && !leading_zero_multi {
*p = q;
true
} else {
*p = start;
false
}
}
fn awk_numf(s: &str) -> f64 {
let s = s.trim_start();
let b = s.as_bytes();
let mut i = 0;
if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
i += 1;
}
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
if i < b.len() && b[i] == b'.' {
i += 1;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
}
s[..i].parse().unwrap_or(0.0)
}
fn fmt_g(x: f64) -> String {
if x == 0.0 {
return "0".to_string();
}
if !x.is_finite() {
return if x.is_nan() {
"nan".to_string()
} else if x < 0.0 {
"-inf".to_string()
} else {
"inf".to_string()
};
}
let p: i32 = 6;
let exp = x.abs().log10().floor() as i32;
if exp < -4 || exp >= p {
let s = format!("{:.*e}", (p - 1) as usize, x);
strip_e(&s)
} else {
let prec = (p - 1 - exp).max(0) as usize;
let s = format!("{x:.prec$}");
strip_f(&s)
}
}
fn strip_f(s: &str) -> String {
if s.contains('.') {
let t = s.trim_end_matches('0');
t.trim_end_matches('.').to_string()
} else {
s.to_string()
}
}
fn strip_e(s: &str) -> String {
if let Some(epos) = s.find('e') {
let (mant, exp) = s.split_at(epos);
let mant = strip_f(mant);
format!("{mant}{exp}")
} else {
s.to_string()
}
}