1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
// Copyright 2020-2021 Ian Jackson and contributors to Otter
// SPDX-License-Identifier: AGPL-3.0-or-later
// There is NO WARRANTY.

use crate::prelude::*;

use parking_lot::{const_rwlock, RwLock};

#[derive(SerializeDisplay)]
#[derive(DeserializeFromStr)]
#[derive(Clone,Debug)]
pub struct Timezone(Arc<TzInfo>);

#[derive(Clone,Debug,Default)]
struct TzInfo {
  name: String,
  ctz: Option<chrono_tz::Tz>,
}

impl Timezone {
  pub fn name(&self) -> &str {
    &self.0.name
  }

  // Oh my god this API is awful!

  #[throws(as Option)]
  fn format_tz<'r, TZ: chrono::TimeZone>(
    tz: &TZ, ts: Timestamp, fmt: &'r str
  ) -> chrono::format::DelayedFormat<chrono::format::StrftimeItems<'r>>
    where <TZ as chrono::TimeZone>::Offset: Display
  {
    use chrono::DateTime;
    let dt = tz.timestamp_opt(ts.0.try_into().ok()?, 0).single()?;
    DateTime::format(&dt, fmt)
  }

  pub fn format(&self, ts: Timestamp) -> String {
    match (||{
      let fmt = "%Y-%m-%d %H:%M:%S %z";
      let df = match &self.0.ctz {
        Some(ctz) => Timezone::format_tz(ctz,                  ts, fmt),
        None      => Timezone::format_tz(&chrono::offset::Utc, ts, fmt),
      }
      .ok_or_else(
        || format!("timestamp {} out of range!", &ts.0)
      )?;
      Ok(format!("{}", df))
    })() {
      Ok(s) => s,
      Err(s) => s,
    }
  }

  pub fn default_todo() -> Self {
    Timezone::from_str("").unwrap()
  }
}

impl Display for Timezone {
  #[throws(fmt::Error)]
  fn fmt(&self, f: &mut Formatter) {
    write!(f, "{}", &self.0.name)?;
  }
}

type MemoTable = Option<HashMap<String, Timezone>>;
static MEMO: RwLock<MemoTable> = const_rwlock(None);

impl FromStr for Timezone {
  type Err = Void;
  #[throws(Void)]
  fn from_str(name: &str) -> Self {
    if name.is_empty() { return default() }

    let get = |memor: &MemoTable| memor.as_ref()?.get(name).map(Clone::clone);
    if let Some(got) = get(&MEMO.read()) { return got }

    // slow path
    let mut memow = MEMO.write();
    if let Some(got) = get(&memow) { return got }

    // really slow path
    let out = {
      let name = name.to_string();
      match chrono_tz::Tz::from_str(&name) {
        Ok(ctz) => {
          Arc::new(TzInfo { name, ctz: Some(ctz) })
        },
        Err(emsg) => {
          error!("Error loading timezone {:?}: {}, using UTC", name, emsg);
          Arc::new(TzInfo { name, ctz: None })
        }
      }
    };
    let out = Timezone(out);
    memow.get_or_insert_with(default)
      .insert(name.to_string(), out.clone());
    out
  }
}

impl Default for Timezone {
  fn default() -> Self {
    Timezone(Arc::new(TzInfo { ctz: None, name: default() }))
  }
}