Oregami
Repositories/oxedyne/fe2o3

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
10use crate::format::FormatPattern;
11use oxedyne_fe2o3_core::prelude::*;
12
13use 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)]
38pub 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
49static LOCALE_DB: OnceLock<HashMap<String, Locale>> = OnceLock::new();
50
51fn 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
141fn get_locale_db() -> &'static HashMap<String, Locale> {
142 LOCALE_DB.get_or_init(init_locale_db)
143}
144
145impl 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
271impl Default for Locale {
272 fn default() -> Self {
273 Self::us()
274 }
275}
276
277impl 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)]
284mod 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}