Skip to main content

polyglot_sql/
time.rs

1//! Time format conversion utilities
2//!
3//! This module provides functionality for converting time format strings
4//! between different SQL dialects. For example, converting Python strftime
5//! formats to Snowflake or BigQuery formats.
6//!
7//! Based on the Python implementation in `sqlglot/time.py`.
8
9use crate::trie::{new_trie_from_keys, Trie, TrieResult};
10use std::collections::{HashMap, HashSet};
11
12/// Convert a time format string using a dialect-specific mapping
13///
14/// This function uses a trie-based algorithm to handle overlapping format
15/// specifiers correctly. For example, `%Y` and `%y` both start with `%`,
16/// so the algorithm needs to find the longest matching specifier.
17///
18/// # Arguments
19/// * `input` - The format string to convert
20/// * `mapping` - A map from source format specifiers to target format specifiers
21/// * `trie` - Optional pre-built trie for the mapping keys (for performance)
22///
23/// # Returns
24/// The converted format string, or `None` if input is empty
25///
26/// # Example
27///
28/// ```
29/// use polyglot_sql::time::format_time;
30/// use std::collections::HashMap;
31///
32/// let mut mapping = HashMap::new();
33/// mapping.insert("%Y", "YYYY");
34/// mapping.insert("%m", "MM");
35/// mapping.insert("%d", "DD");
36///
37/// let result = format_time("%Y-%m-%d", &mapping, None);
38/// assert_eq!(result, Some("YYYY-MM-DD".to_string()));
39/// ```
40pub fn format_time(
41    input: &str,
42    mapping: &HashMap<&str, &str>,
43    trie: Option<&Trie<()>>,
44) -> Option<String> {
45    if input.is_empty() {
46        return None;
47    }
48
49    // Build trie if not provided
50    let owned_trie;
51    let trie = match trie {
52        Some(t) => t,
53        None => {
54            owned_trie = build_format_trie(mapping);
55            &owned_trie
56        }
57    };
58
59    let chars: Vec<char> = input.chars().collect();
60    let size = chars.len();
61    let mut start = 0;
62    let mut end = 1;
63    let mut current = trie;
64    let mut chunks = Vec::new();
65    let mut sym: Option<String> = None;
66
67    while end <= size {
68        let ch = chars[end - 1];
69        let (result, subtrie) = current.in_trie_char(ch);
70
71        match result {
72            TrieResult::Failed => {
73                if let Some(ref matched) = sym {
74                    // We had a previous match, use it
75                    end -= 1;
76                    chunks.push(matched.clone());
77                    start += matched.chars().count();
78                    sym = None;
79                } else {
80                    // No match, emit the first character
81                    chunks.push(chars[start].to_string());
82                    end = start + 1;
83                    start += 1;
84                }
85                current = trie;
86            }
87            TrieResult::Exists => {
88                // Found a complete match, remember it
89                let matched: String = chars[start..end].iter().collect();
90                sym = Some(matched);
91                current = subtrie.unwrap_or(trie);
92            }
93            TrieResult::Prefix => {
94                // Partial match, continue
95                current = subtrie.unwrap_or(trie);
96            }
97        }
98
99        end += 1;
100
101        // At end of string, emit any remaining match
102        if result != TrieResult::Failed && end > size {
103            let matched: String = chars[start..end - 1].iter().collect();
104            chunks.push(matched);
105        }
106    }
107
108    // Apply mapping to chunks
109    let result: String = chunks
110        .iter()
111        .map(|chunk| {
112            mapping
113                .get(chunk.as_str())
114                .map(|s| s.to_string())
115                .unwrap_or_else(|| chunk.clone())
116        })
117        .collect();
118
119    Some(result)
120}
121
122/// Build a trie from format mapping keys
123///
124/// This is useful for performance when the same mapping will be used
125/// multiple times.
126pub fn build_format_trie(mapping: &HashMap<&str, &str>) -> Trie<()> {
127    new_trie_from_keys(mapping.keys().copied())
128}
129
130/// Extract subsecond precision from an ISO timestamp literal
131///
132/// Given a timestamp like '2023-01-01 12:13:14.123456+00:00', returns
133/// the precision (0, 3, or 6) based on the number of subsecond digits.
134///
135/// # Arguments
136/// * `timestamp_literal` - An ISO-8601 timestamp string
137///
138/// # Returns
139/// The precision: 0 (no subseconds), 3 (milliseconds), or 6 (microseconds)
140///
141/// # Example
142///
143/// ```
144/// use polyglot_sql::time::subsecond_precision;
145///
146/// assert_eq!(subsecond_precision("2023-01-01 12:13:14"), 0);
147/// assert_eq!(subsecond_precision("2023-01-01 12:13:14.123"), 3);
148/// assert_eq!(subsecond_precision("2023-01-01 12:13:14.123456"), 6);
149/// ```
150pub fn subsecond_precision(timestamp_literal: &str) -> u8 {
151    // Find the decimal point after seconds
152    let dot_pos = match timestamp_literal.find('.') {
153        Some(pos) => pos,
154        None => return 0,
155    };
156
157    // Find where the fractional part ends (timezone or end of string)
158    let frac_end = timestamp_literal[dot_pos + 1..]
159        .find(|c: char| !c.is_ascii_digit())
160        .map(|pos| pos + dot_pos + 1)
161        .unwrap_or(timestamp_literal.len());
162
163    let frac_len = frac_end - dot_pos - 1;
164
165    // Count significant digits (exclude trailing zeros)
166    let frac_part = &timestamp_literal[dot_pos + 1..frac_end];
167    let significant = frac_part.trim_end_matches('0').len();
168
169    if significant > 3 {
170        6
171    } else if significant > 0 {
172        3
173    } else if frac_len > 0 {
174        // Has fractional part but all zeros - return based on original length
175        if frac_len > 3 {
176            6
177        } else {
178            3
179        }
180    } else {
181        0
182    }
183}
184
185/// Set of valid timezone names (lowercase)
186///
187/// This includes:
188/// - Olson timezone database names (e.g., "america/new_york")
189/// - Timezone abbreviations (e.g., "utc", "est")
190/// - Region-based names
191pub static TIMEZONES: std::sync::LazyLock<HashSet<&'static str>> = std::sync::LazyLock::new(|| {
192    let tzs = [
193        // Africa
194        "africa/abidjan",
195        "africa/accra",
196        "africa/addis_ababa",
197        "africa/algiers",
198        "africa/asmara",
199        "africa/bamako",
200        "africa/bangui",
201        "africa/banjul",
202        "africa/bissau",
203        "africa/blantyre",
204        "africa/brazzaville",
205        "africa/bujumbura",
206        "africa/cairo",
207        "africa/casablanca",
208        "africa/ceuta",
209        "africa/conakry",
210        "africa/dakar",
211        "africa/dar_es_salaam",
212        "africa/djibouti",
213        "africa/douala",
214        "africa/el_aaiun",
215        "africa/freetown",
216        "africa/gaborone",
217        "africa/harare",
218        "africa/johannesburg",
219        "africa/juba",
220        "africa/kampala",
221        "africa/khartoum",
222        "africa/kigali",
223        "africa/kinshasa",
224        "africa/lagos",
225        "africa/libreville",
226        "africa/lome",
227        "africa/luanda",
228        "africa/lubumbashi",
229        "africa/lusaka",
230        "africa/malabo",
231        "africa/maputo",
232        "africa/maseru",
233        "africa/mbabane",
234        "africa/mogadishu",
235        "africa/monrovia",
236        "africa/nairobi",
237        "africa/ndjamena",
238        "africa/niamey",
239        "africa/nouakchott",
240        "africa/ouagadougou",
241        "africa/porto-novo",
242        "africa/sao_tome",
243        "africa/tripoli",
244        "africa/tunis",
245        "africa/windhoek",
246        // America
247        "america/adak",
248        "america/anchorage",
249        "america/anguilla",
250        "america/antigua",
251        "america/araguaina",
252        "america/argentina/buenos_aires",
253        "america/argentina/catamarca",
254        "america/argentina/cordoba",
255        "america/argentina/jujuy",
256        "america/argentina/la_rioja",
257        "america/argentina/mendoza",
258        "america/argentina/rio_gallegos",
259        "america/argentina/salta",
260        "america/argentina/san_juan",
261        "america/argentina/san_luis",
262        "america/argentina/tucuman",
263        "america/argentina/ushuaia",
264        "america/aruba",
265        "america/asuncion",
266        "america/atikokan",
267        "america/bahia",
268        "america/bahia_banderas",
269        "america/barbados",
270        "america/belem",
271        "america/belize",
272        "america/blanc-sablon",
273        "america/boa_vista",
274        "america/bogota",
275        "america/boise",
276        "america/cambridge_bay",
277        "america/campo_grande",
278        "america/cancun",
279        "america/caracas",
280        "america/cayenne",
281        "america/cayman",
282        "america/chicago",
283        "america/chihuahua",
284        "america/ciudad_juarez",
285        "america/costa_rica",
286        "america/creston",
287        "america/cuiaba",
288        "america/curacao",
289        "america/danmarkshavn",
290        "america/dawson",
291        "america/dawson_creek",
292        "america/denver",
293        "america/detroit",
294        "america/dominica",
295        "america/edmonton",
296        "america/eirunepe",
297        "america/el_salvador",
298        "america/fort_nelson",
299        "america/fortaleza",
300        "america/glace_bay",
301        "america/goose_bay",
302        "america/grand_turk",
303        "america/grenada",
304        "america/guadeloupe",
305        "america/guatemala",
306        "america/guayaquil",
307        "america/guyana",
308        "america/halifax",
309        "america/havana",
310        "america/hermosillo",
311        "america/indiana/indianapolis",
312        "america/indiana/knox",
313        "america/indiana/marengo",
314        "america/indiana/petersburg",
315        "america/indiana/tell_city",
316        "america/indiana/vevay",
317        "america/indiana/vincennes",
318        "america/indiana/winamac",
319        "america/inuvik",
320        "america/iqaluit",
321        "america/jamaica",
322        "america/juneau",
323        "america/kentucky/louisville",
324        "america/kentucky/monticello",
325        "america/kralendijk",
326        "america/la_paz",
327        "america/lima",
328        "america/los_angeles",
329        "america/lower_princes",
330        "america/maceio",
331        "america/managua",
332        "america/manaus",
333        "america/marigot",
334        "america/martinique",
335        "america/matamoros",
336        "america/mazatlan",
337        "america/menominee",
338        "america/merida",
339        "america/metlakatla",
340        "america/mexico_city",
341        "america/miquelon",
342        "america/moncton",
343        "america/monterrey",
344        "america/montevideo",
345        "america/montserrat",
346        "america/nassau",
347        "america/new_york",
348        "america/nipigon",
349        "america/nome",
350        "america/noronha",
351        "america/north_dakota/beulah",
352        "america/north_dakota/center",
353        "america/north_dakota/new_salem",
354        "america/nuuk",
355        "america/ojinaga",
356        "america/panama",
357        "america/pangnirtung",
358        "america/paramaribo",
359        "america/phoenix",
360        "america/port-au-prince",
361        "america/port_of_spain",
362        "america/porto_velho",
363        "america/puerto_rico",
364        "america/punta_arenas",
365        "america/rainy_river",
366        "america/rankin_inlet",
367        "america/recife",
368        "america/regina",
369        "america/resolute",
370        "america/rio_branco",
371        "america/santarem",
372        "america/santiago",
373        "america/santo_domingo",
374        "america/sao_paulo",
375        "america/scoresbysund",
376        "america/sitka",
377        "america/st_barthelemy",
378        "america/st_johns",
379        "america/st_kitts",
380        "america/st_lucia",
381        "america/st_thomas",
382        "america/st_vincent",
383        "america/swift_current",
384        "america/tegucigalpa",
385        "america/thule",
386        "america/thunder_bay",
387        "america/tijuana",
388        "america/toronto",
389        "america/tortola",
390        "america/vancouver",
391        "america/whitehorse",
392        "america/winnipeg",
393        "america/yakutat",
394        "america/yellowknife",
395        // Antarctica
396        "antarctica/casey",
397        "antarctica/davis",
398        "antarctica/dumontdurville",
399        "antarctica/macquarie",
400        "antarctica/mawson",
401        "antarctica/mcmurdo",
402        "antarctica/palmer",
403        "antarctica/rothera",
404        "antarctica/syowa",
405        "antarctica/troll",
406        "antarctica/vostok",
407        // Arctic
408        "arctic/longyearbyen",
409        // Asia
410        "asia/aden",
411        "asia/almaty",
412        "asia/amman",
413        "asia/anadyr",
414        "asia/aqtau",
415        "asia/aqtobe",
416        "asia/ashgabat",
417        "asia/atyrau",
418        "asia/baghdad",
419        "asia/bahrain",
420        "asia/baku",
421        "asia/bangkok",
422        "asia/barnaul",
423        "asia/beirut",
424        "asia/bishkek",
425        "asia/brunei",
426        "asia/chita",
427        "asia/choibalsan",
428        "asia/colombo",
429        "asia/damascus",
430        "asia/dhaka",
431        "asia/dili",
432        "asia/dubai",
433        "asia/dushanbe",
434        "asia/famagusta",
435        "asia/gaza",
436        "asia/hebron",
437        "asia/ho_chi_minh",
438        "asia/hong_kong",
439        "asia/hovd",
440        "asia/irkutsk",
441        "asia/jakarta",
442        "asia/jayapura",
443        "asia/jerusalem",
444        "asia/kabul",
445        "asia/kamchatka",
446        "asia/karachi",
447        "asia/kathmandu",
448        "asia/khandyga",
449        "asia/kolkata",
450        "asia/krasnoyarsk",
451        "asia/kuala_lumpur",
452        "asia/kuching",
453        "asia/kuwait",
454        "asia/macau",
455        "asia/magadan",
456        "asia/makassar",
457        "asia/manila",
458        "asia/muscat",
459        "asia/nicosia",
460        "asia/novokuznetsk",
461        "asia/novosibirsk",
462        "asia/omsk",
463        "asia/oral",
464        "asia/phnom_penh",
465        "asia/pontianak",
466        "asia/pyongyang",
467        "asia/qatar",
468        "asia/qostanay",
469        "asia/qyzylorda",
470        "asia/riyadh",
471        "asia/sakhalin",
472        "asia/samarkand",
473        "asia/seoul",
474        "asia/shanghai",
475        "asia/singapore",
476        "asia/srednekolymsk",
477        "asia/taipei",
478        "asia/tashkent",
479        "asia/tbilisi",
480        "asia/tehran",
481        "asia/thimphu",
482        "asia/tokyo",
483        "asia/tomsk",
484        "asia/ulaanbaatar",
485        "asia/urumqi",
486        "asia/ust-nera",
487        "asia/vientiane",
488        "asia/vladivostok",
489        "asia/yakutsk",
490        "asia/yangon",
491        "asia/yekaterinburg",
492        "asia/yerevan",
493        // Atlantic
494        "atlantic/azores",
495        "atlantic/bermuda",
496        "atlantic/canary",
497        "atlantic/cape_verde",
498        "atlantic/faroe",
499        "atlantic/madeira",
500        "atlantic/reykjavik",
501        "atlantic/south_georgia",
502        "atlantic/st_helena",
503        "atlantic/stanley",
504        // Australia
505        "australia/adelaide",
506        "australia/brisbane",
507        "australia/broken_hill",
508        "australia/darwin",
509        "australia/eucla",
510        "australia/hobart",
511        "australia/lindeman",
512        "australia/lord_howe",
513        "australia/melbourne",
514        "australia/perth",
515        "australia/sydney",
516        // Europe
517        "europe/amsterdam",
518        "europe/andorra",
519        "europe/astrakhan",
520        "europe/athens",
521        "europe/belgrade",
522        "europe/berlin",
523        "europe/bratislava",
524        "europe/brussels",
525        "europe/bucharest",
526        "europe/budapest",
527        "europe/busingen",
528        "europe/chisinau",
529        "europe/copenhagen",
530        "europe/dublin",
531        "europe/gibraltar",
532        "europe/guernsey",
533        "europe/helsinki",
534        "europe/isle_of_man",
535        "europe/istanbul",
536        "europe/jersey",
537        "europe/kaliningrad",
538        "europe/kiev",
539        "europe/kirov",
540        "europe/kyiv",
541        "europe/lisbon",
542        "europe/ljubljana",
543        "europe/london",
544        "europe/luxembourg",
545        "europe/madrid",
546        "europe/malta",
547        "europe/mariehamn",
548        "europe/minsk",
549        "europe/monaco",
550        "europe/moscow",
551        "europe/oslo",
552        "europe/paris",
553        "europe/podgorica",
554        "europe/prague",
555        "europe/riga",
556        "europe/rome",
557        "europe/samara",
558        "europe/san_marino",
559        "europe/sarajevo",
560        "europe/saratov",
561        "europe/simferopol",
562        "europe/skopje",
563        "europe/sofia",
564        "europe/stockholm",
565        "europe/tallinn",
566        "europe/tirane",
567        "europe/ulyanovsk",
568        "europe/uzhgorod",
569        "europe/vaduz",
570        "europe/vatican",
571        "europe/vienna",
572        "europe/vilnius",
573        "europe/volgograd",
574        "europe/warsaw",
575        "europe/zagreb",
576        "europe/zaporozhye",
577        "europe/zurich",
578        // Indian
579        "indian/antananarivo",
580        "indian/chagos",
581        "indian/christmas",
582        "indian/cocos",
583        "indian/comoro",
584        "indian/kerguelen",
585        "indian/mahe",
586        "indian/maldives",
587        "indian/mauritius",
588        "indian/mayotte",
589        "indian/reunion",
590        // Pacific
591        "pacific/apia",
592        "pacific/auckland",
593        "pacific/bougainville",
594        "pacific/chatham",
595        "pacific/chuuk",
596        "pacific/easter",
597        "pacific/efate",
598        "pacific/fakaofo",
599        "pacific/fiji",
600        "pacific/funafuti",
601        "pacific/galapagos",
602        "pacific/gambier",
603        "pacific/guadalcanal",
604        "pacific/guam",
605        "pacific/honolulu",
606        "pacific/kanton",
607        "pacific/kiritimati",
608        "pacific/kosrae",
609        "pacific/kwajalein",
610        "pacific/majuro",
611        "pacific/marquesas",
612        "pacific/midway",
613        "pacific/nauru",
614        "pacific/niue",
615        "pacific/norfolk",
616        "pacific/noumea",
617        "pacific/pago_pago",
618        "pacific/palau",
619        "pacific/pitcairn",
620        "pacific/pohnpei",
621        "pacific/port_moresby",
622        "pacific/rarotonga",
623        "pacific/saipan",
624        "pacific/tahiti",
625        "pacific/tarawa",
626        "pacific/tongatapu",
627        "pacific/wake",
628        "pacific/wallis",
629        // Common abbreviations
630        "utc",
631        "gmt",
632        "est",
633        "edt",
634        "cst",
635        "cdt",
636        "mst",
637        "mdt",
638        "pst",
639        "pdt",
640        "cet",
641        "cest",
642        "wet",
643        "west",
644        "eet",
645        "eest",
646        "gmt+0",
647        "gmt-0",
648        "gmt0",
649        "etc/gmt",
650        "etc/utc",
651        "etc/gmt+0",
652        "etc/gmt-0",
653        "etc/gmt+1",
654        "etc/gmt+2",
655        "etc/gmt+3",
656        "etc/gmt+4",
657        "etc/gmt+5",
658        "etc/gmt+6",
659        "etc/gmt+7",
660        "etc/gmt+8",
661        "etc/gmt+9",
662        "etc/gmt+10",
663        "etc/gmt+11",
664        "etc/gmt+12",
665        "etc/gmt-1",
666        "etc/gmt-2",
667        "etc/gmt-3",
668        "etc/gmt-4",
669        "etc/gmt-5",
670        "etc/gmt-6",
671        "etc/gmt-7",
672        "etc/gmt-8",
673        "etc/gmt-9",
674        "etc/gmt-10",
675        "etc/gmt-11",
676        "etc/gmt-12",
677        "etc/gmt-13",
678        "etc/gmt-14",
679    ];
680    tzs.into_iter().collect()
681});
682
683/// Check if a string is a valid timezone name
684///
685/// # Example
686///
687/// ```
688/// use polyglot_sql::time::is_valid_timezone;
689///
690/// assert!(is_valid_timezone("America/New_York"));
691/// assert!(is_valid_timezone("UTC"));
692/// assert!(!is_valid_timezone("Invalid/Timezone"));
693/// ```
694pub fn is_valid_timezone(tz: &str) -> bool {
695    TIMEZONES.contains(tz.to_lowercase().as_str())
696}
697
698/// Common format mapping for Python strftime to various SQL dialects
699pub mod format_mappings {
700    use std::collections::HashMap;
701
702    /// Create a Python strftime to Snowflake format mapping
703    pub fn python_to_snowflake() -> HashMap<&'static str, &'static str> {
704        let mut m = HashMap::new();
705        m.insert("%Y", "YYYY");
706        m.insert("%y", "YY");
707        m.insert("%m", "MM");
708        m.insert("%d", "DD");
709        m.insert("%H", "HH24");
710        m.insert("%I", "HH12");
711        m.insert("%M", "MI");
712        m.insert("%S", "SS");
713        m.insert("%f", "FF6");
714        m.insert("%p", "AM");
715        m.insert("%j", "DDD");
716        m.insert("%W", "WW");
717        m.insert("%w", "D");
718        m.insert("%b", "MON");
719        m.insert("%B", "MONTH");
720        m.insert("%a", "DY");
721        m.insert("%A", "DAY");
722        m.insert("%z", "TZH:TZM");
723        m.insert("%Z", "TZR");
724        m
725    }
726
727    /// Create a Python strftime to BigQuery format mapping
728    pub fn python_to_bigquery() -> HashMap<&'static str, &'static str> {
729        let mut m = HashMap::new();
730        m.insert("%Y", "%Y");
731        m.insert("%y", "%y");
732        m.insert("%m", "%m");
733        m.insert("%d", "%d");
734        m.insert("%H", "%H");
735        m.insert("%I", "%I");
736        m.insert("%M", "%M");
737        m.insert("%S", "%S");
738        m.insert("%f", "%E6S");
739        m.insert("%p", "%p");
740        m.insert("%j", "%j");
741        m.insert("%W", "%W");
742        m.insert("%w", "%w");
743        m.insert("%b", "%b");
744        m.insert("%B", "%B");
745        m.insert("%a", "%a");
746        m.insert("%A", "%A");
747        m.insert("%z", "%z");
748        m.insert("%Z", "%Z");
749        m
750    }
751
752    /// Create a Python strftime to MySQL format mapping
753    pub fn python_to_mysql() -> HashMap<&'static str, &'static str> {
754        let mut m = HashMap::new();
755        m.insert("%Y", "%Y");
756        m.insert("%y", "%y");
757        m.insert("%m", "%m");
758        m.insert("%d", "%d");
759        m.insert("%H", "%H");
760        m.insert("%I", "%h");
761        m.insert("%M", "%i");
762        m.insert("%S", "%s");
763        m.insert("%f", "%f");
764        m.insert("%p", "%p");
765        m.insert("%j", "%j");
766        m.insert("%W", "%U");
767        m.insert("%w", "%w");
768        m.insert("%b", "%b");
769        m.insert("%B", "%M");
770        m.insert("%a", "%a");
771        m.insert("%A", "%W");
772        m
773    }
774
775    /// Create a Python strftime to PostgreSQL format mapping
776    pub fn python_to_postgres() -> HashMap<&'static str, &'static str> {
777        let mut m = HashMap::new();
778        m.insert("%Y", "YYYY");
779        m.insert("%y", "YY");
780        m.insert("%m", "MM");
781        m.insert("%d", "DD");
782        m.insert("%H", "HH24");
783        m.insert("%I", "HH12");
784        m.insert("%M", "MI");
785        m.insert("%S", "SS");
786        m.insert("%f", "US");
787        m.insert("%p", "AM");
788        m.insert("%j", "DDD");
789        m.insert("%W", "WW");
790        m.insert("%w", "D");
791        m.insert("%b", "Mon");
792        m.insert("%B", "Month");
793        m.insert("%a", "Dy");
794        m.insert("%A", "Day");
795        m.insert("%z", "OF");
796        m.insert("%Z", "TZ");
797        m
798    }
799
800    /// Create a Python strftime to Oracle format mapping
801    pub fn python_to_oracle() -> HashMap<&'static str, &'static str> {
802        let mut m = HashMap::new();
803        m.insert("%Y", "YYYY");
804        m.insert("%y", "YY");
805        m.insert("%m", "MM");
806        m.insert("%d", "DD");
807        m.insert("%H", "HH24");
808        m.insert("%I", "HH");
809        m.insert("%M", "MI");
810        m.insert("%S", "SS");
811        m.insert("%f", "FF6");
812        m.insert("%p", "AM");
813        m.insert("%j", "DDD");
814        m.insert("%W", "WW");
815        m.insert("%w", "D");
816        m.insert("%b", "MON");
817        m.insert("%B", "MONTH");
818        m.insert("%a", "DY");
819        m.insert("%A", "DAY");
820        m.insert("%z", "TZH:TZM");
821        m.insert("%Z", "TZR");
822        m
823    }
824
825    /// Create a Python strftime to Spark format mapping
826    pub fn python_to_spark() -> HashMap<&'static str, &'static str> {
827        let mut m = HashMap::new();
828        m.insert("%Y", "yyyy");
829        m.insert("%y", "yy");
830        m.insert("%m", "MM");
831        m.insert("%d", "dd");
832        m.insert("%H", "HH");
833        m.insert("%I", "hh");
834        m.insert("%M", "mm");
835        m.insert("%S", "ss");
836        m.insert("%f", "SSSSSS");
837        m.insert("%p", "a");
838        m.insert("%j", "D");
839        m.insert("%W", "w");
840        m.insert("%w", "u");
841        m.insert("%b", "MMM");
842        m.insert("%B", "MMMM");
843        m.insert("%a", "E");
844        m.insert("%A", "EEEE");
845        m.insert("%z", "XXX");
846        m.insert("%Z", "z");
847        m
848    }
849}
850
851#[cfg(test)]
852mod tests {
853    use super::*;
854
855    #[test]
856    fn test_format_time_basic() {
857        let mut mapping = HashMap::new();
858        mapping.insert("%Y", "YYYY");
859
860        let result = format_time("%Y", &mapping, None);
861        assert_eq!(result, Some("YYYY".to_string()));
862    }
863
864    #[test]
865    fn test_format_time_multiple() {
866        let mut mapping = HashMap::new();
867        mapping.insert("%Y", "YYYY");
868        mapping.insert("%m", "MM");
869        mapping.insert("%d", "DD");
870
871        let result = format_time("%Y-%m-%d", &mapping, None);
872        assert_eq!(result, Some("YYYY-MM-DD".to_string()));
873    }
874
875    #[test]
876    fn test_format_time_empty() {
877        let mapping = HashMap::new();
878        assert_eq!(format_time("", &mapping, None), None);
879    }
880
881    #[test]
882    fn test_format_time_no_mapping() {
883        let mapping = HashMap::new();
884        let result = format_time("hello", &mapping, None);
885        assert_eq!(result, Some("hello".to_string()));
886    }
887
888    #[test]
889    fn test_format_time_partial_match() {
890        let mut mapping = HashMap::new();
891        mapping.insert("%Y", "YYYY");
892        // %y is not in mapping
893
894        let result = format_time("%Y %y", &mapping, None);
895        // %Y matches, %y doesn't match but should pass through
896        assert_eq!(result, Some("YYYY %y".to_string()));
897    }
898
899    #[test]
900    fn test_subsecond_precision_none() {
901        assert_eq!(subsecond_precision("2023-01-01 12:13:14"), 0);
902    }
903
904    #[test]
905    fn test_subsecond_precision_milliseconds() {
906        assert_eq!(subsecond_precision("2023-01-01 12:13:14.123"), 3);
907        assert_eq!(subsecond_precision("2023-01-01 12:13:14.100"), 3);
908    }
909
910    #[test]
911    fn test_subsecond_precision_microseconds() {
912        assert_eq!(subsecond_precision("2023-01-01 12:13:14.123456"), 6);
913        assert_eq!(subsecond_precision("2023-01-01 12:13:14.123456+00:00"), 6);
914    }
915
916    #[test]
917    fn test_subsecond_precision_with_timezone() {
918        assert_eq!(subsecond_precision("2023-01-01 12:13:14.123+00:00"), 3);
919        assert_eq!(subsecond_precision("2023-01-01T12:13:14.123456Z"), 6);
920    }
921
922    #[test]
923    fn test_is_valid_timezone() {
924        assert!(is_valid_timezone("UTC"));
925        assert!(is_valid_timezone("utc"));
926        assert!(is_valid_timezone("America/New_York"));
927        assert!(is_valid_timezone("america/new_york"));
928        assert!(is_valid_timezone("Europe/London"));
929        assert!(!is_valid_timezone("Invalid/Timezone"));
930        assert!(!is_valid_timezone("NotATimezone"));
931    }
932
933    #[test]
934    fn test_python_to_snowflake() {
935        let mapping = format_mappings::python_to_snowflake();
936        let result = format_time("%Y-%m-%d %H:%M:%S", &mapping, None);
937        assert_eq!(result, Some("YYYY-MM-DD HH24:MI:SS".to_string()));
938    }
939
940    #[test]
941    fn test_python_to_spark() {
942        let mapping = format_mappings::python_to_spark();
943        let result = format_time("%Y-%m-%d %H:%M:%S", &mapping, None);
944        assert_eq!(result, Some("yyyy-MM-dd HH:mm:ss".to_string()));
945    }
946}