oxedyne/fe2o3/fe2o3_datime/src/format/locale.rs
11.6 KiB, 46 runs
created by r1870400018:6365, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Locales: the regional conventions that decide how a date or time is written. |
| 2 | //! |
| 3 | //! Each locale carries default patterns for date, time and datetime, together |
| 4 | //! with short and long variants. A small built-in database covers the common |
| 5 | //! ones. |
| 6 | //! |
| 7 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 8 | //! Anthropic Claude |
| 9 | |
| 10 | use crate::format::FormatPattern; |
| 11 | use oxedyne_fe2o3_core::prelude::*; |
| 12 | |
| 13 | use std::{ |
| 14 | collections::HashMap, |
| 15 | sync::OnceLock, |
| 16 | }; |
| 17 | |
| 18 | /// # Examples |
| 19 | /// |
| 20 | /// ```ignore |
| 21 | /// use oxedyne_fe2o3_datime::format::{Locale, CalClockFormatter}res!(); |
| 22 | /// use oxedyne_fe2o3_datime::time::CalClockres!(); |
| 23 | /// |
| 24 | /// let calclock = CalClock::now_utc()?res!(); |
| 25 | /// let formatter = CalClockFormatter::new()res!(); |
| 26 | /// |
| 27 | /// // Use US locale formatting |
| 28 | /// let us_locale = Locale::us()res!(); |
| 29 | /// let us_date = formatter.format_with_pattern(&calclock, us_locale.date_pattern())?res!(); |
| 30 | /// // Result: "01/15/2024" |
| 31 | /// |
| 32 | /// // Use European locale formatting |
| 33 | /// let european_locale = Locale::europe()res!(); |
| 34 | /// let european_date = formatter.format_with_pattern(&calclock, european_locale.date_pattern())?res!(); |
| 35 | /// // Result: "15/01/2024" |
| 36 | /// ``` |
| 37 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] |
| 38 | pub struct Locale { |
| 39 | id: String, // "en-US", "en-GB", "de-DE" |
| 40 | display_name: String, |
| 41 | date_pattern: FormatPattern, |
| 42 | time_pattern: FormatPattern, |
| 43 | datetime_pattern: FormatPattern, |
| 44 | short_date_pattern: FormatPattern, |
| 45 | long_date_pattern: FormatPattern, |
| 46 | short_time_pattern: FormatPattern, |
| 47 | } |
| 48 | |
| 49 | static LOCALE_DB: OnceLock<HashMap<String, Locale>> = OnceLock::new(); |
| 50 | |
| 51 | fn init_locale_db() -> HashMap<String, Locale> { |
| 52 | let mut db = HashMap::new(); |
| 53 | |
| 54 | // United States (en-US) |
| 55 | db.insert("en-US".to_string(), Locale { |
| 56 | id: "en-US".to_string(), |
| 57 | display_name: "English (United States)".to_string(), |
| 58 | date_pattern: FormatPattern::us_date(), |
| 59 | time_pattern: FormatPattern::time_12h(), |
| 60 | datetime_pattern: FormatPattern::new("MM/dd/yyyy h:mm:ss a").unwrap(), |
| 61 | short_date_pattern: FormatPattern::new("M/d/yy").unwrap(), |
| 62 | long_date_pattern: FormatPattern::full_date(), |
| 63 | short_time_pattern: FormatPattern::time_short(), |
| 64 | }); |
| 65 | |
| 66 | // United Kingdom (en-GB) |
| 67 | db.insert("en-GB".to_string(), Locale { |
| 68 | id: "en-GB".to_string(), |
| 69 | display_name: "English (United Kingdom)".to_string(), |
| 70 | date_pattern: FormatPattern::new("dd/MM/yyyy").unwrap(), |
| 71 | time_pattern: FormatPattern::time_24h(), |
| 72 | datetime_pattern: FormatPattern::new("dd/MM/yyyy HH:mm:ss").unwrap(), |
| 73 | short_date_pattern: FormatPattern::new("d/M/yy").unwrap(), |
| 74 | long_date_pattern: FormatPattern::full_date(), |
| 75 | short_time_pattern: FormatPattern::new("HH:mm").unwrap(), |
| 76 | }); |
| 77 | |
| 78 | // Germany (de-DE) |
| 79 | db.insert("de-DE".to_string(), Locale { |
| 80 | id: "de-DE".to_string(), |
| 81 | display_name: "Deutsch (Deutschland)".to_string(), |
| 82 | date_pattern: FormatPattern::new("dd.MM.yyyy").unwrap(), |
| 83 | time_pattern: FormatPattern::time_24h(), |
| 84 | datetime_pattern: FormatPattern::new("dd.MM.yyyy HH:mm:ss").unwrap(), |
| 85 | short_date_pattern: FormatPattern::new("d.M.yy").unwrap(), |
| 86 | long_date_pattern: FormatPattern::new("EEEE, d. MMMM yyyy").unwrap(), |
| 87 | short_time_pattern: FormatPattern::new("HH:mm").unwrap(), |
| 88 | }); |
| 89 | |
| 90 | // France (fr-FR) |
| 91 | db.insert("fr-FR".to_string(), Locale { |
| 92 | id: "fr-FR".to_string(), |
| 93 | display_name: "Français (France)".to_string(), |
| 94 | date_pattern: FormatPattern::new("dd/MM/yyyy").unwrap(), |
| 95 | time_pattern: FormatPattern::time_24h(), |
| 96 | datetime_pattern: FormatPattern::new("dd/MM/yyyy HH:mm:ss").unwrap(), |
| 97 | short_date_pattern: FormatPattern::new("d/M/yy").unwrap(), |
| 98 | long_date_pattern: FormatPattern::new("EEEE d MMMM yyyy").unwrap(), |
| 99 | short_time_pattern: FormatPattern::new("HH:mm").unwrap(), |
| 100 | }); |
| 101 | |
| 102 | // Japan (ja-JP) |
| 103 | db.insert("ja-JP".to_string(), Locale { |
| 104 | id: "ja-JP".to_string(), |
| 105 | display_name: "日本語 (日本)".to_string(), |
| 106 | date_pattern: FormatPattern::new("yyyy/MM/dd").unwrap(), |
| 107 | time_pattern: FormatPattern::time_24h(), |
| 108 | datetime_pattern: FormatPattern::new("yyyy/MM/dd HH:mm:ss").unwrap(), |
| 109 | short_date_pattern: FormatPattern::new("yy/M/d").unwrap(), |
| 110 | long_date_pattern: FormatPattern::new("yyyy'年'M'月'd'日' EEEE").unwrap(), |
| 111 | short_time_pattern: FormatPattern::new("HH:mm").unwrap(), |
| 112 | }); |
| 113 | |
| 114 | // China (zh-CN) |
| 115 | db.insert("zh-CN".to_string(), Locale { |
| 116 | id: "zh-CN".to_string(), |
| 117 | display_name: "中文 (中国)".to_string(), |
| 118 | date_pattern: FormatPattern::new("yyyy/M/d").unwrap(), |
| 119 | time_pattern: FormatPattern::time_24h(), |
| 120 | datetime_pattern: FormatPattern::new("yyyy/M/d HH:mm:ss").unwrap(), |
| 121 | short_date_pattern: FormatPattern::new("yy/M/d").unwrap(), |
| 122 | long_date_pattern: FormatPattern::new("yyyy'年'M'月'd'日' EEEE").unwrap(), |
| 123 | short_time_pattern: FormatPattern::new("HH:mm").unwrap(), |
| 124 | }); |
| 125 | |
| 126 | // ISO 8601 (International Standard) |
| 127 | db.insert("ISO".to_string(), Locale { |
| 128 | id: "ISO".to_string(), |
| 129 | display_name: "ISO 8601 International Standard".to_string(), |
| 130 | date_pattern: FormatPattern::iso_date(), |
| 131 | time_pattern: FormatPattern::iso_time(), |
| 132 | datetime_pattern: FormatPattern::iso_datetime(), |
| 133 | short_date_pattern: FormatPattern::iso_date(), |
| 134 | long_date_pattern: FormatPattern::iso_date(), |
| 135 | short_time_pattern: FormatPattern::new("HH:mm").unwrap(), |
| 136 | }); |
| 137 | |
| 138 | db |
| 139 | } |
| 140 | |
| 141 | fn get_locale_db() -> &'static HashMap<String, Locale> { |
| 142 | LOCALE_DB.get_or_init(init_locale_db) |
| 143 | } |
| 144 | |
| 145 | impl Locale { |
| 146 | pub fn new<S: Into<String>>( |
| 147 | id: S, |
| 148 | display_name: S, |
| 149 | date_pattern: FormatPattern, |
| 150 | time_pattern: FormatPattern, |
| 151 | datetime_pattern: FormatPattern, |
| 152 | ) -> Self { |
| 153 | let id_str = id.into(); |
| 154 | Self { |
| 155 | id: id_str.clone(), |
| 156 | display_name: display_name.into(), |
| 157 | short_date_pattern: date_pattern.clone(), |
| 158 | long_date_pattern: FormatPattern::full_date(), |
| 159 | short_time_pattern: FormatPattern::time_short(), |
| 160 | date_pattern, |
| 161 | time_pattern, |
| 162 | datetime_pattern, |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | /// An identifier that is not in the built-in database falls back to en-US. |
| 167 | /// |
| 168 | /// # Examples |
| 169 | /// |
| 170 | /// ```ignore |
| 171 | /// let us_locale = Locale::from_id("en-US")res!(); |
| 172 | /// let german_locale = Locale::from_id("de-DE")res!(); |
| 173 | /// let fallback = Locale::from_id("unknown")res!(); // Returns en-US |
| 174 | /// ``` |
| 175 | pub fn from_id<S: Into<String>>(locale_id: S) -> Self { |
| 176 | let id = locale_id.into(); |
| 177 | |
| 178 | if let Some(locale) = get_locale_db().get(&id) { |
| 179 | locale.clone() |
| 180 | } else { |
| 181 | // Fall back to US English |
| 182 | Self::us() |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | pub fn id(&self) -> &str { |
| 187 | &self.id |
| 188 | } |
| 189 | |
| 190 | pub fn display_name(&self) -> &str { |
| 191 | &self.display_name |
| 192 | } |
| 193 | |
| 194 | pub fn date_pattern(&self) -> &FormatPattern { |
| 195 | &self.date_pattern |
| 196 | } |
| 197 | |
| 198 | pub fn time_pattern(&self) -> &FormatPattern { |
| 199 | &self.time_pattern |
| 200 | } |
| 201 | |
| 202 | pub fn datetime_pattern(&self) -> &FormatPattern { |
| 203 | &self.datetime_pattern |
| 204 | } |
| 205 | |
| 206 | pub fn short_date_pattern(&self) -> &FormatPattern { |
| 207 | &self.short_date_pattern |
| 208 | } |
| 209 | |
| 210 | pub fn long_date_pattern(&self) -> &FormatPattern { |
| 211 | &self.long_date_pattern |
| 212 | } |
| 213 | |
| 214 | pub fn short_time_pattern(&self) -> &FormatPattern { |
| 215 | &self.short_time_pattern |
| 216 | } |
| 217 | |
| 218 | // ======================================================================== |
| 219 | // Predefined Locale Constructors |
| 220 | // ======================================================================== |
| 221 | |
| 222 | pub fn us() -> Self { |
| 223 | Self::from_id("en-US") // MM/dd/yyyy, 12-hour |
| 224 | } |
| 225 | |
| 226 | pub fn uk() -> Self { |
| 227 | Self::from_id("en-GB") // dd/MM/yyyy, 24-hour |
| 228 | } |
| 229 | |
| 230 | pub fn germany() -> Self { |
| 231 | Self::from_id("de-DE") // dd.MM.yyyy, 24-hour |
| 232 | } |
| 233 | |
| 234 | pub fn france() -> Self { |
| 235 | Self::from_id("fr-FR") // dd/MM/yyyy, 24-hour |
| 236 | } |
| 237 | |
| 238 | pub fn japan() -> Self { |
| 239 | Self::from_id("ja-JP") // yyyy/MM/dd, 24-hour |
| 240 | } |
| 241 | |
| 242 | pub fn china() -> Self { |
| 243 | Self::from_id("zh-CN") // yyyy/M/d, 24-hour |
| 244 | } |
| 245 | |
| 246 | pub fn iso() -> Self { |
| 247 | Self::from_id("ISO") // yyyy-MM-dd, HH:mm:ss |
| 248 | } |
| 249 | |
| 250 | pub fn europe() -> Self { |
| 251 | Self::uk() |
| 252 | } |
| 253 | |
| 254 | pub fn available_locales() -> Vec<String> { |
| 255 | let mut locales: Vec<String> = get_locale_db().keys().cloned().collect(); |
| 256 | locales.sort(); |
| 257 | locales |
| 258 | } |
| 259 | |
| 260 | /// Identifier first, then display name. |
| 261 | pub fn available_locales_with_names() -> Vec<(String, String)> { |
| 262 | let mut locales: Vec<(String, String)> = get_locale_db() |
| 263 | .values() |
| 264 | .map(|locale| (locale.id.clone(), locale.display_name.clone())) |
| 265 | .collect(); |
| 266 | locales.sort_by(|a, b| a.0.cmp(&b.0)); |
| 267 | locales |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | impl Default for Locale { |
| 272 | fn default() -> Self { |
| 273 | Self::us() |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | impl std::fmt::Display for Locale { |
| 278 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 279 | write!(f, "{} ({})", self.display_name, self.id) |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | #[cfg(test)] |
| 284 | mod tests { |
| 285 | use super::*; |
| 286 | |
| 287 | #[test] |
| 288 | fn test_locale_creation() { |
| 289 | let us_locale = Locale::us(); |
| 290 | assert_eq!(us_locale.id(), "en-US"); |
| 291 | assert_eq!(us_locale.display_name(), "English (United States)"); |
| 292 | } |
| 293 | |
| 294 | #[test] |
| 295 | fn test_locale_from_id() { |
| 296 | let german_locale = Locale::from_id("de-DE"); |
| 297 | assert_eq!(german_locale.id(), "de-DE"); |
| 298 | assert_eq!(german_locale.display_name(), "Deutsch (Deutschland)"); |
| 299 | |
| 300 | // Test fallback to US for unknown locale |
| 301 | let unknown_locale = Locale::from_id("xx-XX"); |
| 302 | assert_eq!(unknown_locale.id(), "en-US"); |
| 303 | } |
| 304 | |
| 305 | #[test] |
| 306 | fn test_pattern_access() { |
| 307 | let us_locale = Locale::us(); |
| 308 | |
| 309 | // US locale should use MM/dd/yyyy date format |
| 310 | assert_eq!(us_locale.date_pattern().pattern_string(), "MM/dd/yyyy"); |
| 311 | |
| 312 | // US locale should use 12-hour time format |
| 313 | assert_eq!(us_locale.time_pattern().pattern_string(), "h:mm:ss a"); |
| 314 | |
| 315 | let german_locale = Locale::germany(); |
| 316 | |
| 317 | // German locale should use dd.MM.yyyy date format |
| 318 | assert_eq!(german_locale.date_pattern().pattern_string(), "dd.MM.yyyy"); |
| 319 | |
| 320 | // German locale should use 24-hour time format |
| 321 | assert_eq!(german_locale.time_pattern().pattern_string(), "HH:mm:ss"); |
| 322 | } |
| 323 | |
| 324 | #[test] |
| 325 | fn test_available_locales() { |
| 326 | let locales = Locale::available_locales(); |
| 327 | assert!(locales.contains(&"en-US".to_string())); |
| 328 | assert!(locales.contains(&"de-DE".to_string())); |
| 329 | assert!(locales.contains(&"ja-JP".to_string())); |
| 330 | assert!(locales.len() >= 7); // At least the predefined locales |
| 331 | } |
| 332 | |
| 333 | #[test] |
| 334 | fn test_locale_display() { |
| 335 | let us_locale = Locale::us(); |
| 336 | let display_string = format!("{}", us_locale); |
| 337 | assert!(display_string.contains("English (United States)")); |
| 338 | assert!(display_string.contains("en-US")); |
| 339 | } |
| 340 | |
| 341 | #[test] |
| 342 | fn test_iso_locale() { |
| 343 | let iso_locale = Locale::iso(); |
| 344 | assert_eq!(iso_locale.id(), "ISO"); |
| 345 | assert_eq!(iso_locale.date_pattern().pattern_string(), "yyyy-MM-dd"); |
| 346 | assert_eq!(iso_locale.time_pattern().pattern_string(), "HH:mm:ss"); |
| 347 | } |
| 348 | } |