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>> =
192    std::sync::LazyLock::new(|| {
193        let tzs = [
194            // Africa
195            "africa/abidjan",
196            "africa/accra",
197            "africa/addis_ababa",
198            "africa/algiers",
199            "africa/asmara",
200            "africa/bamako",
201            "africa/bangui",
202            "africa/banjul",
203            "africa/bissau",
204            "africa/blantyre",
205            "africa/brazzaville",
206            "africa/bujumbura",
207            "africa/cairo",
208            "africa/casablanca",
209            "africa/ceuta",
210            "africa/conakry",
211            "africa/dakar",
212            "africa/dar_es_salaam",
213            "africa/djibouti",
214            "africa/douala",
215            "africa/el_aaiun",
216            "africa/freetown",
217            "africa/gaborone",
218            "africa/harare",
219            "africa/johannesburg",
220            "africa/juba",
221            "africa/kampala",
222            "africa/khartoum",
223            "africa/kigali",
224            "africa/kinshasa",
225            "africa/lagos",
226            "africa/libreville",
227            "africa/lome",
228            "africa/luanda",
229            "africa/lubumbashi",
230            "africa/lusaka",
231            "africa/malabo",
232            "africa/maputo",
233            "africa/maseru",
234            "africa/mbabane",
235            "africa/mogadishu",
236            "africa/monrovia",
237            "africa/nairobi",
238            "africa/ndjamena",
239            "africa/niamey",
240            "africa/nouakchott",
241            "africa/ouagadougou",
242            "africa/porto-novo",
243            "africa/sao_tome",
244            "africa/tripoli",
245            "africa/tunis",
246            "africa/windhoek",
247            // America
248            "america/adak",
249            "america/anchorage",
250            "america/anguilla",
251            "america/antigua",
252            "america/araguaina",
253            "america/argentina/buenos_aires",
254            "america/argentina/catamarca",
255            "america/argentina/cordoba",
256            "america/argentina/jujuy",
257            "america/argentina/la_rioja",
258            "america/argentina/mendoza",
259            "america/argentina/rio_gallegos",
260            "america/argentina/salta",
261            "america/argentina/san_juan",
262            "america/argentina/san_luis",
263            "america/argentina/tucuman",
264            "america/argentina/ushuaia",
265            "america/aruba",
266            "america/asuncion",
267            "america/atikokan",
268            "america/bahia",
269            "america/bahia_banderas",
270            "america/barbados",
271            "america/belem",
272            "america/belize",
273            "america/blanc-sablon",
274            "america/boa_vista",
275            "america/bogota",
276            "america/boise",
277            "america/cambridge_bay",
278            "america/campo_grande",
279            "america/cancun",
280            "america/caracas",
281            "america/cayenne",
282            "america/cayman",
283            "america/chicago",
284            "america/chihuahua",
285            "america/ciudad_juarez",
286            "america/costa_rica",
287            "america/creston",
288            "america/cuiaba",
289            "america/curacao",
290            "america/danmarkshavn",
291            "america/dawson",
292            "america/dawson_creek",
293            "america/denver",
294            "america/detroit",
295            "america/dominica",
296            "america/edmonton",
297            "america/eirunepe",
298            "america/el_salvador",
299            "america/fort_nelson",
300            "america/fortaleza",
301            "america/glace_bay",
302            "america/goose_bay",
303            "america/grand_turk",
304            "america/grenada",
305            "america/guadeloupe",
306            "america/guatemala",
307            "america/guayaquil",
308            "america/guyana",
309            "america/halifax",
310            "america/havana",
311            "america/hermosillo",
312            "america/indiana/indianapolis",
313            "america/indiana/knox",
314            "america/indiana/marengo",
315            "america/indiana/petersburg",
316            "america/indiana/tell_city",
317            "america/indiana/vevay",
318            "america/indiana/vincennes",
319            "america/indiana/winamac",
320            "america/inuvik",
321            "america/iqaluit",
322            "america/jamaica",
323            "america/juneau",
324            "america/kentucky/louisville",
325            "america/kentucky/monticello",
326            "america/kralendijk",
327            "america/la_paz",
328            "america/lima",
329            "america/los_angeles",
330            "america/lower_princes",
331            "america/maceio",
332            "america/managua",
333            "america/manaus",
334            "america/marigot",
335            "america/martinique",
336            "america/matamoros",
337            "america/mazatlan",
338            "america/menominee",
339            "america/merida",
340            "america/metlakatla",
341            "america/mexico_city",
342            "america/miquelon",
343            "america/moncton",
344            "america/monterrey",
345            "america/montevideo",
346            "america/montserrat",
347            "america/nassau",
348            "america/new_york",
349            "america/nipigon",
350            "america/nome",
351            "america/noronha",
352            "america/north_dakota/beulah",
353            "america/north_dakota/center",
354            "america/north_dakota/new_salem",
355            "america/nuuk",
356            "america/ojinaga",
357            "america/panama",
358            "america/pangnirtung",
359            "america/paramaribo",
360            "america/phoenix",
361            "america/port-au-prince",
362            "america/port_of_spain",
363            "america/porto_velho",
364            "america/puerto_rico",
365            "america/punta_arenas",
366            "america/rainy_river",
367            "america/rankin_inlet",
368            "america/recife",
369            "america/regina",
370            "america/resolute",
371            "america/rio_branco",
372            "america/santarem",
373            "america/santiago",
374            "america/santo_domingo",
375            "america/sao_paulo",
376            "america/scoresbysund",
377            "america/sitka",
378            "america/st_barthelemy",
379            "america/st_johns",
380            "america/st_kitts",
381            "america/st_lucia",
382            "america/st_thomas",
383            "america/st_vincent",
384            "america/swift_current",
385            "america/tegucigalpa",
386            "america/thule",
387            "america/thunder_bay",
388            "america/tijuana",
389            "america/toronto",
390            "america/tortola",
391            "america/vancouver",
392            "america/whitehorse",
393            "america/winnipeg",
394            "america/yakutat",
395            "america/yellowknife",
396            // Antarctica
397            "antarctica/casey",
398            "antarctica/davis",
399            "antarctica/dumontdurville",
400            "antarctica/macquarie",
401            "antarctica/mawson",
402            "antarctica/mcmurdo",
403            "antarctica/palmer",
404            "antarctica/rothera",
405            "antarctica/syowa",
406            "antarctica/troll",
407            "antarctica/vostok",
408            // Arctic
409            "arctic/longyearbyen",
410            // Asia
411            "asia/aden",
412            "asia/almaty",
413            "asia/amman",
414            "asia/anadyr",
415            "asia/aqtau",
416            "asia/aqtobe",
417            "asia/ashgabat",
418            "asia/atyrau",
419            "asia/baghdad",
420            "asia/bahrain",
421            "asia/baku",
422            "asia/bangkok",
423            "asia/barnaul",
424            "asia/beirut",
425            "asia/bishkek",
426            "asia/brunei",
427            "asia/chita",
428            "asia/choibalsan",
429            "asia/colombo",
430            "asia/damascus",
431            "asia/dhaka",
432            "asia/dili",
433            "asia/dubai",
434            "asia/dushanbe",
435            "asia/famagusta",
436            "asia/gaza",
437            "asia/hebron",
438            "asia/ho_chi_minh",
439            "asia/hong_kong",
440            "asia/hovd",
441            "asia/irkutsk",
442            "asia/jakarta",
443            "asia/jayapura",
444            "asia/jerusalem",
445            "asia/kabul",
446            "asia/kamchatka",
447            "asia/karachi",
448            "asia/kathmandu",
449            "asia/khandyga",
450            "asia/kolkata",
451            "asia/krasnoyarsk",
452            "asia/kuala_lumpur",
453            "asia/kuching",
454            "asia/kuwait",
455            "asia/macau",
456            "asia/magadan",
457            "asia/makassar",
458            "asia/manila",
459            "asia/muscat",
460            "asia/nicosia",
461            "asia/novokuznetsk",
462            "asia/novosibirsk",
463            "asia/omsk",
464            "asia/oral",
465            "asia/phnom_penh",
466            "asia/pontianak",
467            "asia/pyongyang",
468            "asia/qatar",
469            "asia/qostanay",
470            "asia/qyzylorda",
471            "asia/riyadh",
472            "asia/sakhalin",
473            "asia/samarkand",
474            "asia/seoul",
475            "asia/shanghai",
476            "asia/singapore",
477            "asia/srednekolymsk",
478            "asia/taipei",
479            "asia/tashkent",
480            "asia/tbilisi",
481            "asia/tehran",
482            "asia/thimphu",
483            "asia/tokyo",
484            "asia/tomsk",
485            "asia/ulaanbaatar",
486            "asia/urumqi",
487            "asia/ust-nera",
488            "asia/vientiane",
489            "asia/vladivostok",
490            "asia/yakutsk",
491            "asia/yangon",
492            "asia/yekaterinburg",
493            "asia/yerevan",
494            // Atlantic
495            "atlantic/azores",
496            "atlantic/bermuda",
497            "atlantic/canary",
498            "atlantic/cape_verde",
499            "atlantic/faroe",
500            "atlantic/madeira",
501            "atlantic/reykjavik",
502            "atlantic/south_georgia",
503            "atlantic/st_helena",
504            "atlantic/stanley",
505            // Australia
506            "australia/adelaide",
507            "australia/brisbane",
508            "australia/broken_hill",
509            "australia/darwin",
510            "australia/eucla",
511            "australia/hobart",
512            "australia/lindeman",
513            "australia/lord_howe",
514            "australia/melbourne",
515            "australia/perth",
516            "australia/sydney",
517            // Europe
518            "europe/amsterdam",
519            "europe/andorra",
520            "europe/astrakhan",
521            "europe/athens",
522            "europe/belgrade",
523            "europe/berlin",
524            "europe/bratislava",
525            "europe/brussels",
526            "europe/bucharest",
527            "europe/budapest",
528            "europe/busingen",
529            "europe/chisinau",
530            "europe/copenhagen",
531            "europe/dublin",
532            "europe/gibraltar",
533            "europe/guernsey",
534            "europe/helsinki",
535            "europe/isle_of_man",
536            "europe/istanbul",
537            "europe/jersey",
538            "europe/kaliningrad",
539            "europe/kiev",
540            "europe/kirov",
541            "europe/kyiv",
542            "europe/lisbon",
543            "europe/ljubljana",
544            "europe/london",
545            "europe/luxembourg",
546            "europe/madrid",
547            "europe/malta",
548            "europe/mariehamn",
549            "europe/minsk",
550            "europe/monaco",
551            "europe/moscow",
552            "europe/oslo",
553            "europe/paris",
554            "europe/podgorica",
555            "europe/prague",
556            "europe/riga",
557            "europe/rome",
558            "europe/samara",
559            "europe/san_marino",
560            "europe/sarajevo",
561            "europe/saratov",
562            "europe/simferopol",
563            "europe/skopje",
564            "europe/sofia",
565            "europe/stockholm",
566            "europe/tallinn",
567            "europe/tirane",
568            "europe/ulyanovsk",
569            "europe/uzhgorod",
570            "europe/vaduz",
571            "europe/vatican",
572            "europe/vienna",
573            "europe/vilnius",
574            "europe/volgograd",
575            "europe/warsaw",
576            "europe/zagreb",
577            "europe/zaporozhye",
578            "europe/zurich",
579            // Indian
580            "indian/antananarivo",
581            "indian/chagos",
582            "indian/christmas",
583            "indian/cocos",
584            "indian/comoro",
585            "indian/kerguelen",
586            "indian/mahe",
587            "indian/maldives",
588            "indian/mauritius",
589            "indian/mayotte",
590            "indian/reunion",
591            // Pacific
592            "pacific/apia",
593            "pacific/auckland",
594            "pacific/bougainville",
595            "pacific/chatham",
596            "pacific/chuuk",
597            "pacific/easter",
598            "pacific/efate",
599            "pacific/fakaofo",
600            "pacific/fiji",
601            "pacific/funafuti",
602            "pacific/galapagos",
603            "pacific/gambier",
604            "pacific/guadalcanal",
605            "pacific/guam",
606            "pacific/honolulu",
607            "pacific/kanton",
608            "pacific/kiritimati",
609            "pacific/kosrae",
610            "pacific/kwajalein",
611            "pacific/majuro",
612            "pacific/marquesas",
613            "pacific/midway",
614            "pacific/nauru",
615            "pacific/niue",
616            "pacific/norfolk",
617            "pacific/noumea",
618            "pacific/pago_pago",
619            "pacific/palau",
620            "pacific/pitcairn",
621            "pacific/pohnpei",
622            "pacific/port_moresby",
623            "pacific/rarotonga",
624            "pacific/saipan",
625            "pacific/tahiti",
626            "pacific/tarawa",
627            "pacific/tongatapu",
628            "pacific/wake",
629            "pacific/wallis",
630            // Common abbreviations
631            "utc",
632            "gmt",
633            "est",
634            "edt",
635            "cst",
636            "cdt",
637            "mst",
638            "mdt",
639            "pst",
640            "pdt",
641            "cet",
642            "cest",
643            "wet",
644            "west",
645            "eet",
646            "eest",
647            "gmt+0",
648            "gmt-0",
649            "gmt0",
650            "etc/gmt",
651            "etc/utc",
652            "etc/gmt+0",
653            "etc/gmt-0",
654            "etc/gmt+1",
655            "etc/gmt+2",
656            "etc/gmt+3",
657            "etc/gmt+4",
658            "etc/gmt+5",
659            "etc/gmt+6",
660            "etc/gmt+7",
661            "etc/gmt+8",
662            "etc/gmt+9",
663            "etc/gmt+10",
664            "etc/gmt+11",
665            "etc/gmt+12",
666            "etc/gmt-1",
667            "etc/gmt-2",
668            "etc/gmt-3",
669            "etc/gmt-4",
670            "etc/gmt-5",
671            "etc/gmt-6",
672            "etc/gmt-7",
673            "etc/gmt-8",
674            "etc/gmt-9",
675            "etc/gmt-10",
676            "etc/gmt-11",
677            "etc/gmt-12",
678            "etc/gmt-13",
679            "etc/gmt-14",
680        ];
681        tzs.into_iter().collect()
682    });
683
684/// Check if a string is a valid timezone name
685///
686/// # Example
687///
688/// ```
689/// use polyglot_sql::time::is_valid_timezone;
690///
691/// assert!(is_valid_timezone("America/New_York"));
692/// assert!(is_valid_timezone("UTC"));
693/// assert!(!is_valid_timezone("Invalid/Timezone"));
694/// ```
695pub fn is_valid_timezone(tz: &str) -> bool {
696    TIMEZONES.contains(tz.to_lowercase().as_str())
697}
698
699/// Common format mapping for Python strftime to various SQL dialects
700pub mod format_mappings {
701    use std::collections::HashMap;
702
703    /// Create a Python strftime to Snowflake format mapping
704    pub fn python_to_snowflake() -> HashMap<&'static str, &'static str> {
705        let mut m = HashMap::new();
706        m.insert("%Y", "YYYY");
707        m.insert("%y", "YY");
708        m.insert("%m", "MM");
709        m.insert("%d", "DD");
710        m.insert("%H", "HH24");
711        m.insert("%I", "HH12");
712        m.insert("%M", "MI");
713        m.insert("%S", "SS");
714        m.insert("%f", "FF6");
715        m.insert("%p", "AM");
716        m.insert("%j", "DDD");
717        m.insert("%W", "WW");
718        m.insert("%w", "D");
719        m.insert("%b", "MON");
720        m.insert("%B", "MONTH");
721        m.insert("%a", "DY");
722        m.insert("%A", "DAY");
723        m.insert("%z", "TZH:TZM");
724        m.insert("%Z", "TZR");
725        m
726    }
727
728    /// Create a Python strftime to BigQuery format mapping
729    pub fn python_to_bigquery() -> HashMap<&'static str, &'static str> {
730        let mut m = HashMap::new();
731        m.insert("%Y", "%Y");
732        m.insert("%y", "%y");
733        m.insert("%m", "%m");
734        m.insert("%d", "%d");
735        m.insert("%H", "%H");
736        m.insert("%I", "%I");
737        m.insert("%M", "%M");
738        m.insert("%S", "%S");
739        m.insert("%f", "%E6S");
740        m.insert("%p", "%p");
741        m.insert("%j", "%j");
742        m.insert("%W", "%W");
743        m.insert("%w", "%w");
744        m.insert("%b", "%b");
745        m.insert("%B", "%B");
746        m.insert("%a", "%a");
747        m.insert("%A", "%A");
748        m.insert("%z", "%z");
749        m.insert("%Z", "%Z");
750        m
751    }
752
753    /// Create a Python strftime to MySQL format mapping
754    pub fn python_to_mysql() -> HashMap<&'static str, &'static str> {
755        let mut m = HashMap::new();
756        m.insert("%Y", "%Y");
757        m.insert("%y", "%y");
758        m.insert("%m", "%m");
759        m.insert("%d", "%d");
760        m.insert("%H", "%H");
761        m.insert("%I", "%h");
762        m.insert("%M", "%i");
763        m.insert("%S", "%s");
764        m.insert("%f", "%f");
765        m.insert("%p", "%p");
766        m.insert("%j", "%j");
767        m.insert("%W", "%U");
768        m.insert("%w", "%w");
769        m.insert("%b", "%b");
770        m.insert("%B", "%M");
771        m.insert("%a", "%a");
772        m.insert("%A", "%W");
773        m
774    }
775
776    /// Create a Python strftime to PostgreSQL format mapping
777    pub fn python_to_postgres() -> HashMap<&'static str, &'static str> {
778        let mut m = HashMap::new();
779        m.insert("%Y", "YYYY");
780        m.insert("%y", "YY");
781        m.insert("%m", "MM");
782        m.insert("%d", "DD");
783        m.insert("%H", "HH24");
784        m.insert("%I", "HH12");
785        m.insert("%M", "MI");
786        m.insert("%S", "SS");
787        m.insert("%f", "US");
788        m.insert("%p", "AM");
789        m.insert("%j", "DDD");
790        m.insert("%W", "WW");
791        m.insert("%w", "D");
792        m.insert("%b", "Mon");
793        m.insert("%B", "Month");
794        m.insert("%a", "Dy");
795        m.insert("%A", "Day");
796        m.insert("%z", "OF");
797        m.insert("%Z", "TZ");
798        m
799    }
800
801    /// Create a Python strftime to Oracle format mapping
802    pub fn python_to_oracle() -> HashMap<&'static str, &'static str> {
803        let mut m = HashMap::new();
804        m.insert("%Y", "YYYY");
805        m.insert("%y", "YY");
806        m.insert("%m", "MM");
807        m.insert("%d", "DD");
808        m.insert("%H", "HH24");
809        m.insert("%I", "HH");
810        m.insert("%M", "MI");
811        m.insert("%S", "SS");
812        m.insert("%f", "FF6");
813        m.insert("%p", "AM");
814        m.insert("%j", "DDD");
815        m.insert("%W", "WW");
816        m.insert("%w", "D");
817        m.insert("%b", "MON");
818        m.insert("%B", "MONTH");
819        m.insert("%a", "DY");
820        m.insert("%A", "DAY");
821        m.insert("%z", "TZH:TZM");
822        m.insert("%Z", "TZR");
823        m
824    }
825
826    /// Create a Python strftime to Spark format mapping
827    pub fn python_to_spark() -> HashMap<&'static str, &'static str> {
828        let mut m = HashMap::new();
829        m.insert("%Y", "yyyy");
830        m.insert("%y", "yy");
831        m.insert("%m", "MM");
832        m.insert("%d", "dd");
833        m.insert("%H", "HH");
834        m.insert("%I", "hh");
835        m.insert("%M", "mm");
836        m.insert("%S", "ss");
837        m.insert("%f", "SSSSSS");
838        m.insert("%p", "a");
839        m.insert("%j", "D");
840        m.insert("%W", "w");
841        m.insert("%w", "u");
842        m.insert("%b", "MMM");
843        m.insert("%B", "MMMM");
844        m.insert("%a", "E");
845        m.insert("%A", "EEEE");
846        m.insert("%z", "XXX");
847        m.insert("%Z", "z");
848        m
849    }
850}
851
852#[cfg(test)]
853mod tests {
854    use super::*;
855
856    #[test]
857    fn test_format_time_basic() {
858        let mut mapping = HashMap::new();
859        mapping.insert("%Y", "YYYY");
860
861        let result = format_time("%Y", &mapping, None);
862        assert_eq!(result, Some("YYYY".to_string()));
863    }
864
865    #[test]
866    fn test_format_time_multiple() {
867        let mut mapping = HashMap::new();
868        mapping.insert("%Y", "YYYY");
869        mapping.insert("%m", "MM");
870        mapping.insert("%d", "DD");
871
872        let result = format_time("%Y-%m-%d", &mapping, None);
873        assert_eq!(result, Some("YYYY-MM-DD".to_string()));
874    }
875
876    #[test]
877    fn test_format_time_empty() {
878        let mapping = HashMap::new();
879        assert_eq!(format_time("", &mapping, None), None);
880    }
881
882    #[test]
883    fn test_format_time_no_mapping() {
884        let mapping = HashMap::new();
885        let result = format_time("hello", &mapping, None);
886        assert_eq!(result, Some("hello".to_string()));
887    }
888
889    #[test]
890    fn test_format_time_partial_match() {
891        let mut mapping = HashMap::new();
892        mapping.insert("%Y", "YYYY");
893        // %y is not in mapping
894
895        let result = format_time("%Y %y", &mapping, None);
896        // %Y matches, %y doesn't match but should pass through
897        assert_eq!(result, Some("YYYY %y".to_string()));
898    }
899
900    #[test]
901    fn test_subsecond_precision_none() {
902        assert_eq!(subsecond_precision("2023-01-01 12:13:14"), 0);
903    }
904
905    #[test]
906    fn test_subsecond_precision_milliseconds() {
907        assert_eq!(subsecond_precision("2023-01-01 12:13:14.123"), 3);
908        assert_eq!(subsecond_precision("2023-01-01 12:13:14.100"), 3);
909    }
910
911    #[test]
912    fn test_subsecond_precision_microseconds() {
913        assert_eq!(subsecond_precision("2023-01-01 12:13:14.123456"), 6);
914        assert_eq!(subsecond_precision("2023-01-01 12:13:14.123456+00:00"), 6);
915    }
916
917    #[test]
918    fn test_subsecond_precision_with_timezone() {
919        assert_eq!(
920            subsecond_precision("2023-01-01 12:13:14.123+00:00"),
921            3
922        );
923        assert_eq!(
924            subsecond_precision("2023-01-01T12:13:14.123456Z"),
925            6
926        );
927    }
928
929    #[test]
930    fn test_is_valid_timezone() {
931        assert!(is_valid_timezone("UTC"));
932        assert!(is_valid_timezone("utc"));
933        assert!(is_valid_timezone("America/New_York"));
934        assert!(is_valid_timezone("america/new_york"));
935        assert!(is_valid_timezone("Europe/London"));
936        assert!(!is_valid_timezone("Invalid/Timezone"));
937        assert!(!is_valid_timezone("NotATimezone"));
938    }
939
940    #[test]
941    fn test_python_to_snowflake() {
942        let mapping = format_mappings::python_to_snowflake();
943        let result = format_time("%Y-%m-%d %H:%M:%S", &mapping, None);
944        assert_eq!(result, Some("YYYY-MM-DD HH24:MI:SS".to_string()));
945    }
946
947    #[test]
948    fn test_python_to_spark() {
949        let mapping = format_mappings::python_to_spark();
950        let result = format_time("%Y-%m-%d %H:%M:%S", &mapping, None);
951        assert_eq!(result, Some("yyyy-MM-dd HH:mm:ss".to_string()));
952    }
953}