【Chrono库】Unix-like 系统时区处理实现(src/offset/local/unix.rs)

这段代码是 Chrono 时间库在 Unix-like 系统上的时区处理实现,通过 IANA 时区数据库来处理本地时间和 UTC 时间的转换。

核心架构

线程本地缓存

rust 复制代码
thread_local! {
    static TZ_INFO: RefCell<Option<Cache>> = Default::default();
}
  • 使用线程本地存储缓存时区信息
  • 避免重复解析时区数据
  • 支持多线程环境

统一的接口函数

rust 复制代码
pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
    offset(utc, false)
}

pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
    offset(local, true)
}

核心数据结构

Cache 结构体

rust 复制代码
struct Cache {
    zone: TimeZone,        // 当前时区信息
    source: Source,        // 时区数据来源
    last_checked: SystemTime, // 最后检查时间
}

Source 枚举

标识时区信息的来源:

  • LocalTime { mtime: SystemTime }: 来自 /etc/localtime 文件
  • Environment { hash: u64 }: 来自 TZ 环境变量

关键特性

1. 智能缓存机制

rust 复制代码
// 1秒内无条件重用缓存
Ok(d) if d.as_secs() < 1 => (),
// 超过1秒检查时区是否变化
Ok(_) | Err(_) => {
    // 检查时区源是否变化
    let out_of_date = match (&self.source, &new_source) {
        // 环境变量和文件来源切换
        (Source::Environment { .. }, Source::LocalTime { .. }) | ... => true,
        // 文件修改时间变化
        (Source::LocalTime { mtime: old_mtime }, Source::LocalTime { mtime })
            if old_mtime != mtime => true,
        // 环境变量内容变化
        (Source::Environment { hash: old_hash }, Source::Environment { hash })
            if old_hash != hash => true,
        // 缓存可重用
        _ => false,
    };
}

2. 时区发现策略

rust 复制代码
fn current_zone(var: Option<&str>) -> TimeZone {
    TimeZone::local(var).ok()
        .or_else(fallback_timezone)
        .unwrap_or_else(TimeZone::utc)  // 最终回退到 UTC
}

时区发现优先级:

  1. TZ 环境变量指定的时区
  2. 系统 /etc/localtime 文件
  3. IANA 时区数据库自动检测
  4. 回退到 UTC

3. 平台特定的时区数据库路径

rust 复制代码
#[cfg(target_os = "aix")]
const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo";

#[cfg(not(any(target_os = "android", target_os = "aix", target_env = "ohos")))]
const TZDB_LOCATION: &str = "/usr/share/zoneinfo";

核心算法

UTC 到本地时间转换

rust 复制代码
if !local {
    let offset = self.zone
        .find_local_time_type(d.and_utc().timestamp())
        .expect("unable to select local time type")
        .offset();
    
    FixedOffset::east_opt(offset)
}

本地时间到 UTC 转换

rust 复制代码
self.zone
    .find_local_time_type_from_local(d)
    .expect("unable to select local time type")
    .and_then(|o| FixedOffset::east_opt(o.offset()))

设计优势

1. 性能优化

  • 1秒缓存窗口: 平衡性能与时区变化响应速度
  • 哈希比较: 环境变量变化时快速检测
  • 文件修改时间: 检测系统时区配置变化

2. 健壮性

  • 多层回退机制: 确保总能获得有效的时区信息
  • 错误处理: 在时区解析失败时优雅降级到 UTC
  • 平台适配: 支持多种 Unix-like 系统

3. 标准兼容

  • 遵循 IANA 时区数据库标准
  • 支持 TZ 环境变量覆盖
  • 兼容系统 /etc/localtime 配置

这个实现提供了在 Unix-like 系统上高效、准确的时区处理能力,通过智能缓存和多重回退机制确保在各种环境下都能可靠工作。

复制代码
## 附源码

```rust
// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::{cell::RefCell, collections::hash_map, env, fs, hash::Hasher, time::SystemTime};

use super::tz_info::TimeZone;
use super::{FixedOffset, NaiveDateTime};
use crate::MappedLocalTime;

pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
    offset(utc, false)
}

pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
    offset(local, true)
}

fn offset(d: &NaiveDateTime, local: bool) -> MappedLocalTime<FixedOffset> {
    TZ_INFO.with(|maybe_cache| {
        maybe_cache.borrow_mut().get_or_insert_with(Cache::default).offset(*d, local)
    })
}

// we have to store the `Cache` in an option as it can't
// be initialized in a static context.
thread_local! {
    static TZ_INFO: RefCell<Option<Cache>> = Default::default();
}

enum Source {
    LocalTime { mtime: SystemTime },
    Environment { hash: u64 },
}

impl Source {
    fn new(env_tz: Option<&str>) -> Source {
        match env_tz {
            Some(tz) => {
                let mut hasher = hash_map::DefaultHasher::new();
                hasher.write(tz.as_bytes());
                let hash = hasher.finish();
                Source::Environment { hash }
            }
            None => match fs::symlink_metadata("/etc/localtime") {
                Ok(data) => Source::LocalTime {
                    // we have to pick a sensible default when the mtime fails
                    // by picking SystemTime::now() we raise the probability of
                    // the cache being invalidated if/when the mtime starts working
                    mtime: data.modified().unwrap_or_else(|_| SystemTime::now()),
                },
                Err(_) => {
                    // as above, now() should be a better default than some constant
                    // TODO: see if we can improve caching in the case where the fallback is a valid timezone
                    Source::LocalTime { mtime: SystemTime::now() }
                }
            },
        }
    }
}

struct Cache {
    zone: TimeZone,
    source: Source,
    last_checked: SystemTime,
}

#[cfg(target_os = "aix")]
const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo";

#[cfg(not(any(target_os = "android", target_os = "aix", target_env = "ohos")))]
const TZDB_LOCATION: &str = "/usr/share/zoneinfo";

fn fallback_timezone() -> Option<TimeZone> {
    let tz_name = iana_time_zone::get_timezone().ok()?;
    #[cfg(not(any(target_os = "android", target_env = "ohos")))]
    let bytes = fs::read(format!("{TZDB_LOCATION}/{tz_name}")).ok()?;
    #[cfg(any(target_os = "android", target_env = "ohos"))]
    let bytes = crate::offset::local::tz_data::for_zone(&tz_name).ok()??;
    TimeZone::from_tz_data(&bytes).ok()
}

impl Default for Cache {
    fn default() -> Cache {
        // default to UTC if no local timezone can be found
        let env_tz = env::var("TZ").ok();
        let env_ref = env_tz.as_deref();
        Cache {
            last_checked: SystemTime::now(),
            source: Source::new(env_ref),
            zone: current_zone(env_ref),
        }
    }
}

fn current_zone(var: Option<&str>) -> TimeZone {
    TimeZone::local(var).ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc)
}

impl Cache {
    fn offset(&mut self, d: NaiveDateTime, local: bool) -> MappedLocalTime<FixedOffset> {
        let now = SystemTime::now();

        match now.duration_since(self.last_checked) {
            // If the cache has been around for less than a second then we reuse it
            // unconditionally. This is a reasonable tradeoff because the timezone
            // generally won't be changing _that_ often, but if the time zone does
            // change, it will reflect sufficiently quickly from an application
            // user's perspective.
            Ok(d) if d.as_secs() < 1 => (),
            Ok(_) | Err(_) => {
                let env_tz = env::var("TZ").ok();
                let env_ref = env_tz.as_deref();
                let new_source = Source::new(env_ref);

                let out_of_date = match (&self.source, &new_source) {
                    // change from env to file or file to env, must recreate the zone
                    (Source::Environment { .. }, Source::LocalTime { .. })
                    | (Source::LocalTime { .. }, Source::Environment { .. }) => true,
                    // stay as file, but mtime has changed
                    (Source::LocalTime { mtime: old_mtime }, Source::LocalTime { mtime })
                        if old_mtime != mtime =>
                    {
                        true
                    }
                    // stay as env, but hash of variable has changed
                    (Source::Environment { hash: old_hash }, Source::Environment { hash })
                        if old_hash != hash =>
                    {
                        true
                    }
                    // cache can be reused
                    _ => false,
                };

                if out_of_date {
                    self.zone = current_zone(env_ref);
                }

                self.last_checked = now;
                self.source = new_source;
            }
        }

        if !local {
            let offset = self
                .zone
                .find_local_time_type(d.and_utc().timestamp())
                .expect("unable to select local time type")
                .offset();

            return match FixedOffset::east_opt(offset) {
                Some(offset) => MappedLocalTime::Single(offset),
                None => MappedLocalTime::None,
            };
        }

        // we pass through the year as the year of a local point in time must either be valid in that locale, or
        // the entire time was skipped in which case we will return MappedLocalTime::None anyway.
        self.zone
            .find_local_time_type_from_local(d)
            .expect("unable to select local time type")
            .and_then(|o| FixedOffset::east_opt(o.offset()))
    }
}

```
相关推荐
DongLi011 天前
rustlings 学习笔记 -- exercises/05_vecs
rust
番茄灭世神2 天前
Rust学习笔记第2篇
rust·编程语言
shimly1234562 天前
(done) 速通 rustlings(20) 错误处理1 --- 不涉及Traits
rust
shimly1234562 天前
(done) 速通 rustlings(19) Option
rust
@atweiwei2 天前
rust所有权机制详解
开发语言·数据结构·后端·rust·内存·所有权
shimly1234562 天前
(done) 速通 rustlings(24) 错误处理2 --- 涉及Traits
rust
shimly1234562 天前
(done) 速通 rustlings(23) 特性 Traits
rust
shimly1234562 天前
(done) 速通 rustlings(17) 哈希表
rust
shimly1234563 天前
(done) 速通 rustlings(15) 字符串
rust
shimly1234563 天前
(done) 速通 rustlings(22) 泛型
rust