bckt 0.7.3

bckt is an opinionated but flexible static site generator for blogs
use anyhow::{Context, Result, bail};
use time::UtcOffset;

pub fn parse_timezone(value: &str) -> Result<UtcOffset> {
    if value.eq_ignore_ascii_case("UTC") || value.eq_ignore_ascii_case("Z") {
        return Ok(UtcOffset::UTC);
    }

    let trimmed = value.trim();
    let mut chars = trimmed.chars();
    let sign_char = chars
        .next()
        .with_context(|| format!("default_timezone '{}' is empty", value))?;
    let sign = match sign_char {
        '+' => 1,
        '-' => -1,
        _ => bail!("default_timezone must start with '+' or '-'"),
    };

    let remainder = chars.as_str();
    let mut parts = remainder.split(':');
    let hours_str = parts
        .next()
        .with_context(|| format!("default_timezone '{}' missing hour component", value))?;
    let minutes_str = parts.next().unwrap_or("0");
    let seconds_str = parts.next().unwrap_or("0");

    if parts.next().is_some() {
        bail!("default_timezone '{}' has too many components", value);
    }

    let hours: i8 = hours_str
        .parse()
        .with_context(|| format!("default_timezone '{}' hour component invalid", value))?;
    let minutes: i8 = minutes_str
        .parse()
        .with_context(|| format!("default_timezone '{}' minute component invalid", value))?;
    let seconds: i8 = seconds_str
        .parse()
        .with_context(|| format!("default_timezone '{}' second component invalid", value))?;

    UtcOffset::from_hms(sign * hours, sign * minutes, sign * seconds)
        .with_context(|| format!("default_timezone '{}' out of range", value))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_utc_variants() {
        assert!(parse_timezone("UTC").is_ok());
        assert!(parse_timezone("Z").is_ok());
        assert!(parse_timezone("utc").is_ok());
    }

    #[test]
    fn parse_positive_offset() {
        let offset = parse_timezone("+05:30").unwrap();
        assert_eq!(offset.whole_hours(), 5);
    }

    #[test]
    fn parse_negative_offset() {
        let offset = parse_timezone("-08:00").unwrap();
        assert_eq!(offset.whole_hours(), -8);
    }

    #[test]
    fn reject_invalid_format() {
        assert!(parse_timezone("Mars/Station").is_err());
        assert!(parse_timezone("invalid").is_err());
    }
}