Skip to main content

number_date_normalizer/
number_date_normalizer.rs

1//! Number/Date Normalizer FST Example
2//!
3//! This example demonstrates how to build and use finite state transducers for
4//! normalizing various textual representations of numbers, dates, times, and
5//! other structured data into standardized formats. It shows:
6//! 1. Converting written numbers to digits (e.g., "twenty-three" → "23")
7//! 2. Normalizing date formats (e.g., "Jan 15, 2024" → "2024-01-15")
8//! 3. Time format standardization (e.g., "3:30 PM" → "15:30")
9//! 4. Currency and measurement normalization
10//! 5. Ordinal number handling (e.g., "first" → "1st")
11//! 6. Phone number and address normalization
12//!
13//! This is essential for text preprocessing in NLP, data extraction, and
14//! information normalization systems.
15//!
16//! Usage:
17//! ```bash
18//! cargo run --example number_date_normalizer
19//! ```
20
21use arcweight::prelude::*;
22
23/// Types of normalizable entities
24#[derive(Debug, Clone, PartialEq)]
25enum NormalizationType {
26    Number,
27    Date,
28    Time,
29    Currency,
30    Measurement,
31    Ordinal,
32    PhoneNumber,
33}
34
35/// Normalized entity result
36#[derive(Debug, Clone)]
37struct NormalizedEntity {
38    original: String,
39    normalized: String,
40    entity_type: NormalizationType,
41    _confidence: f32,
42}
43
44/// Number normalizer database - maps written numbers to digits
45struct NumberNormalizer {
46    word_to_digit: Vec<(String, String)>,
47    ordinal_to_number: Vec<(String, String)>,
48    date_patterns: Vec<(String, String)>,
49    time_patterns: Vec<(String, String)>,
50    currency_patterns: Vec<(String, String)>,
51    measurement_patterns: Vec<(String, String)>,
52}
53
54impl NumberNormalizer {
55    fn new() -> Self {
56        let word_to_digit = vec![
57            // Basic numbers 0-19
58            ("zero".to_string(), "0".to_string()),
59            ("one".to_string(), "1".to_string()),
60            ("two".to_string(), "2".to_string()),
61            ("three".to_string(), "3".to_string()),
62            ("four".to_string(), "4".to_string()),
63            ("five".to_string(), "5".to_string()),
64            ("six".to_string(), "6".to_string()),
65            ("seven".to_string(), "7".to_string()),
66            ("eight".to_string(), "8".to_string()),
67            ("nine".to_string(), "9".to_string()),
68            ("ten".to_string(), "10".to_string()),
69            ("eleven".to_string(), "11".to_string()),
70            ("twelve".to_string(), "12".to_string()),
71            ("thirteen".to_string(), "13".to_string()),
72            ("fourteen".to_string(), "14".to_string()),
73            ("fifteen".to_string(), "15".to_string()),
74            ("sixteen".to_string(), "16".to_string()),
75            ("seventeen".to_string(), "17".to_string()),
76            ("eighteen".to_string(), "18".to_string()),
77            ("nineteen".to_string(), "19".to_string()),
78            // Tens
79            ("twenty".to_string(), "20".to_string()),
80            ("thirty".to_string(), "30".to_string()),
81            ("forty".to_string(), "40".to_string()),
82            ("fifty".to_string(), "50".to_string()),
83            ("sixty".to_string(), "60".to_string()),
84            ("seventy".to_string(), "70".to_string()),
85            ("eighty".to_string(), "80".to_string()),
86            ("ninety".to_string(), "90".to_string()),
87            // Compound numbers
88            ("twenty-one".to_string(), "21".to_string()),
89            ("twenty-two".to_string(), "22".to_string()),
90            ("twenty-three".to_string(), "23".to_string()),
91            ("thirty-five".to_string(), "35".to_string()),
92            ("forty-seven".to_string(), "47".to_string()),
93            ("fifty-nine".to_string(), "59".to_string()),
94            // Hundreds
95            ("hundred".to_string(), "100".to_string()),
96            ("one hundred".to_string(), "100".to_string()),
97            ("two hundred".to_string(), "200".to_string()),
98            ("three hundred".to_string(), "300".to_string()),
99            // Thousands
100            ("thousand".to_string(), "1000".to_string()),
101            ("one thousand".to_string(), "1000".to_string()),
102            ("two thousand".to_string(), "2000".to_string()),
103            // Millions
104            ("million".to_string(), "1000000".to_string()),
105            ("one million".to_string(), "1000000".to_string()),
106        ];
107
108        let ordinal_to_number = vec![
109            ("first".to_string(), "1st".to_string()),
110            ("second".to_string(), "2nd".to_string()),
111            ("third".to_string(), "3rd".to_string()),
112            ("fourth".to_string(), "4th".to_string()),
113            ("fifth".to_string(), "5th".to_string()),
114            ("sixth".to_string(), "6th".to_string()),
115            ("seventh".to_string(), "7th".to_string()),
116            ("eighth".to_string(), "8th".to_string()),
117            ("ninth".to_string(), "9th".to_string()),
118            ("tenth".to_string(), "10th".to_string()),
119            ("eleventh".to_string(), "11th".to_string()),
120            ("twelfth".to_string(), "12th".to_string()),
121            ("thirteenth".to_string(), "13th".to_string()),
122            ("fourteenth".to_string(), "14th".to_string()),
123            ("fifteenth".to_string(), "15th".to_string()),
124            ("twentieth".to_string(), "20th".to_string()),
125            ("twenty-first".to_string(), "21st".to_string()),
126            ("thirtieth".to_string(), "30th".to_string()),
127        ];
128
129        let date_patterns = vec![
130            // Month abbreviations
131            ("Jan".to_string(), "01".to_string()),
132            ("Feb".to_string(), "02".to_string()),
133            ("Mar".to_string(), "03".to_string()),
134            ("Apr".to_string(), "04".to_string()),
135            ("May".to_string(), "05".to_string()),
136            ("Jun".to_string(), "06".to_string()),
137            ("Jul".to_string(), "07".to_string()),
138            ("Aug".to_string(), "08".to_string()),
139            ("Sep".to_string(), "09".to_string()),
140            ("Oct".to_string(), "10".to_string()),
141            ("Nov".to_string(), "11".to_string()),
142            ("Dec".to_string(), "12".to_string()),
143            // Full month names
144            ("January".to_string(), "01".to_string()),
145            ("February".to_string(), "02".to_string()),
146            ("March".to_string(), "03".to_string()),
147            ("April".to_string(), "04".to_string()),
148            ("June".to_string(), "06".to_string()),
149            ("July".to_string(), "07".to_string()),
150            ("August".to_string(), "08".to_string()),
151            ("September".to_string(), "09".to_string()),
152            ("October".to_string(), "10".to_string()),
153            ("November".to_string(), "11".to_string()),
154            ("December".to_string(), "12".to_string()),
155            // Common date formats
156            ("Jan 15, 2024".to_string(), "2024-01-15".to_string()),
157            ("February 3, 2023".to_string(), "2023-02-03".to_string()),
158            ("Mar 22, 2024".to_string(), "2024-03-22".to_string()),
159            ("12/25/2023".to_string(), "2023-12-25".to_string()),
160            ("01/01/2024".to_string(), "2024-01-01".to_string()),
161            ("3/15/24".to_string(), "2024-03-15".to_string()),
162            ("15-Jan-2024".to_string(), "2024-01-15".to_string()),
163            ("2024/01/15".to_string(), "2024-01-15".to_string()),
164        ];
165
166        let time_patterns = vec![
167            // 12-hour format
168            ("12:00 AM".to_string(), "00:00".to_string()),
169            ("1:00 AM".to_string(), "01:00".to_string()),
170            ("12:00 PM".to_string(), "12:00".to_string()),
171            ("1:00 PM".to_string(), "13:00".to_string()),
172            ("2:30 PM".to_string(), "14:30".to_string()),
173            ("3:45 PM".to_string(), "15:45".to_string()),
174            ("6:15 PM".to_string(), "18:15".to_string()),
175            ("11:59 PM".to_string(), "23:59".to_string()),
176            // Common time expressions
177            ("noon".to_string(), "12:00".to_string()),
178            ("midnight".to_string(), "00:00".to_string()),
179            ("quarter past three".to_string(), "15:15".to_string()),
180            ("half past four".to_string(), "16:30".to_string()),
181            ("quarter to five".to_string(), "16:45".to_string()),
182            // Informal time
183            ("3 o'clock".to_string(), "15:00".to_string()),
184            ("8 AM".to_string(), "08:00".to_string()),
185            ("5 PM".to_string(), "17:00".to_string()),
186        ];
187
188        let currency_patterns = vec![
189            // Dollar amounts
190            ("$10".to_string(), "USD 10.00".to_string()),
191            ("$25.50".to_string(), "USD 25.50".to_string()),
192            ("$1,000".to_string(), "USD 1000.00".to_string()),
193            ("$1.5 million".to_string(), "USD 1500000.00".to_string()),
194            ("ten dollars".to_string(), "USD 10.00".to_string()),
195            ("fifty cents".to_string(), "USD 0.50".to_string()),
196            ("a dollar".to_string(), "USD 1.00".to_string()),
197            // Other currencies
198            ("€100".to_string(), "EUR 100.00".to_string()),
199            ("£50".to_string(), "GBP 50.00".to_string()),
200            ("¥1000".to_string(), "JPY 1000.00".to_string()),
201            ("100 euros".to_string(), "EUR 100.00".to_string()),
202            ("fifty pounds".to_string(), "GBP 50.00".to_string()),
203        ];
204
205        let measurement_patterns = vec![
206            // Length
207            ("5 feet".to_string(), "5 ft".to_string()),
208            ("10 inches".to_string(), "10 in".to_string()),
209            ("2 miles".to_string(), "2 mi".to_string()),
210            ("100 meters".to_string(), "100 m".to_string()),
211            ("5 kilometers".to_string(), "5 km".to_string()),
212            ("6 foot 2".to_string(), "6'2\"".to_string()),
213            // Weight
214            ("10 pounds".to_string(), "10 lbs".to_string()),
215            ("2 kilograms".to_string(), "2 kg".to_string()),
216            ("5 ounces".to_string(), "5 oz".to_string()),
217            ("1 ton".to_string(), "1 ton".to_string()),
218            // Volume
219            ("1 gallon".to_string(), "1 gal".to_string()),
220            ("2 liters".to_string(), "2 L".to_string()),
221            ("8 cups".to_string(), "8 cups".to_string()),
222            ("3 tablespoons".to_string(), "3 tbsp".to_string()),
223            // Temperature
224            ("32 degrees Fahrenheit".to_string(), "32°F".to_string()),
225            ("100 degrees Celsius".to_string(), "100°C".to_string()),
226            ("room temperature".to_string(), "20°C".to_string()),
227        ];
228
229        NumberNormalizer {
230            word_to_digit,
231            ordinal_to_number,
232            date_patterns,
233            time_patterns,
234            currency_patterns,
235            measurement_patterns,
236        }
237    }
238
239    /// Normalize a text containing various entities
240    fn normalize_text(&self, text: &str) -> Vec<NormalizedEntity> {
241        let mut results = Vec::new();
242
243        // Check for number words
244        for (word, digit) in &self.word_to_digit {
245            if text.contains(word) {
246                results.push(NormalizedEntity {
247                    original: word.clone(),
248                    normalized: digit.clone(),
249                    entity_type: NormalizationType::Number,
250                    _confidence: 1.0,
251                });
252            }
253        }
254
255        // Check for ordinal numbers
256        for (ordinal, number) in &self.ordinal_to_number {
257            if text.contains(ordinal) {
258                results.push(NormalizedEntity {
259                    original: ordinal.clone(),
260                    normalized: number.clone(),
261                    entity_type: NormalizationType::Ordinal,
262                    _confidence: 1.0,
263                });
264            }
265        }
266
267        // Check for date patterns
268        for (date_text, normalized_date) in &self.date_patterns {
269            if text.contains(date_text) {
270                results.push(NormalizedEntity {
271                    original: date_text.clone(),
272                    normalized: normalized_date.clone(),
273                    entity_type: NormalizationType::Date,
274                    _confidence: 1.0,
275                });
276            }
277        }
278
279        // Check for time patterns
280        for (time_text, normalized_time) in &self.time_patterns {
281            if text.contains(time_text) {
282                results.push(NormalizedEntity {
283                    original: time_text.clone(),
284                    normalized: normalized_time.clone(),
285                    entity_type: NormalizationType::Time,
286                    _confidence: 1.0,
287                });
288            }
289        }
290
291        // Check for currency patterns
292        for (currency_text, normalized_currency) in &self.currency_patterns {
293            if text.contains(currency_text) {
294                results.push(NormalizedEntity {
295                    original: currency_text.clone(),
296                    normalized: normalized_currency.clone(),
297                    entity_type: NormalizationType::Currency,
298                    _confidence: 1.0,
299                });
300            }
301        }
302
303        // Check for measurement patterns
304        for (measurement_text, normalized_measurement) in &self.measurement_patterns {
305            if text.contains(measurement_text) {
306                results.push(NormalizedEntity {
307                    original: measurement_text.clone(),
308                    normalized: normalized_measurement.clone(),
309                    entity_type: NormalizationType::Measurement,
310                    _confidence: 1.0,
311                });
312            }
313        }
314
315        results
316    }
317
318    /// Apply normalizations to a text string
319    fn apply_normalizations(&self, text: &str) -> String {
320        let mut result = text.to_string();
321
322        // Apply number normalizations
323        for (word, digit) in &self.word_to_digit {
324            result = result.replace(word, digit);
325        }
326
327        // Apply ordinal normalizations
328        for (ordinal, number) in &self.ordinal_to_number {
329            result = result.replace(ordinal, number);
330        }
331
332        // Apply date normalizations
333        for (date_text, normalized_date) in &self.date_patterns {
334            result = result.replace(date_text, normalized_date);
335        }
336
337        // Apply time normalizations
338        for (time_text, normalized_time) in &self.time_patterns {
339            result = result.replace(time_text, normalized_time);
340        }
341
342        // Apply currency normalizations
343        for (currency_text, normalized_currency) in &self.currency_patterns {
344            result = result.replace(currency_text, normalized_currency);
345        }
346
347        // Apply measurement normalizations
348        for (measurement_text, normalized_measurement) in &self.measurement_patterns {
349            result = result.replace(measurement_text, normalized_measurement);
350        }
351
352        result
353    }
354}
355
356/// Build a simple FST for number normalization (demonstration)
357fn build_number_normalization_fst() -> VectorFst<TropicalWeight> {
358    let mut fst = VectorFst::new();
359    let start = fst.add_state();
360    fst.set_start(start);
361    fst.set_final(start, TropicalWeight::one());
362
363    // Add some simple number transformations
364    let number_rules = vec![
365        ("one", "1"),
366        ("two", "2"),
367        ("three", "3"),
368        ("four", "4"),
369        ("five", "5"),
370    ];
371
372    for (word, digit) in number_rules {
373        let mut current = start;
374
375        // Accept the word
376        for ch in word.chars() {
377            let next = fst.add_state();
378            fst.add_arc(
379                current,
380                Arc::new(
381                    ch as u32,
382                    0, // epsilon output during word
383                    TropicalWeight::one(),
384                    next,
385                ),
386            );
387            current = next;
388        }
389
390        // Output the digit
391        for ch in digit.chars() {
392            let next = fst.add_state();
393            fst.add_arc(
394                current,
395                Arc::new(
396                    0, // epsilon input
397                    ch as u32,
398                    TropicalWeight::one(),
399                    next,
400                ),
401            );
402            current = next;
403        }
404
405        // Connect back to start for more normalizations
406        fst.add_arc(
407            current,
408            Arc::new(
409                0, // epsilon
410                0, // epsilon
411                TropicalWeight::one(),
412                start,
413            ),
414        );
415    }
416
417    fst
418}
419
420/// Phone number normalizer patterns
421fn normalize_phone_numbers(text: &str) -> Vec<NormalizedEntity> {
422    let phone_patterns = vec![
423        ("(555) 123-4567", "+1-555-123-4567"),
424        ("555-123-4567", "+1-555-123-4567"),
425        ("555.123.4567", "+1-555-123-4567"),
426        ("5551234567", "+1-555-123-4567"),
427        ("+1 555 123 4567", "+1-555-123-4567"),
428        ("1-800-FLOWERS", "+1-800-356-9377"),
429    ];
430
431    let mut results = Vec::new();
432    for (pattern, normalized) in phone_patterns {
433        if text.contains(pattern) {
434            results.push(NormalizedEntity {
435                original: pattern.to_string(),
436                normalized: normalized.to_string(),
437                entity_type: NormalizationType::PhoneNumber,
438                _confidence: 0.95,
439            });
440        }
441    }
442    results
443}
444
445/// Demonstrate FST-based normalization pipeline
446fn process_with_fst_pipeline(text: &str, _normalizer_fst: &VectorFst<TropicalWeight>) -> String {
447    // This is a simplified demonstration of how an FST could be used
448    // In practice, you'd compose the input text FST with the normalizer FST
449
450    // For demonstration, we'll show the concept
451    let simple_replacements = vec![("one", "1"), ("two", "2"), ("three", "3")];
452
453    let mut result = text.to_string();
454    for (from, to) in simple_replacements {
455        result = result.replace(from, to);
456    }
457
458    result
459}
460
461fn main() -> Result<()> {
462    println!("Number/Date Normalizer FST Example");
463    println!("=================================\n");
464
465    // Create normalizer
466    let normalizer = NumberNormalizer::new();
467
468    // Build demonstration FST
469    let _number_fst = build_number_normalization_fst();
470
471    println!("Normalization patterns loaded:");
472    let word_to_digit_len = normalizer.word_to_digit.len();
473    println!("  {word_to_digit_len} number word mappings");
474    let ordinal_to_number_len = normalizer.ordinal_to_number.len();
475    println!("  {ordinal_to_number_len} ordinal number mappings");
476    let date_patterns_len = normalizer.date_patterns.len();
477    println!("  {date_patterns_len} date format patterns");
478    let time_patterns_len = normalizer.time_patterns.len();
479    println!("  {time_patterns_len} time format patterns");
480    let currency_patterns_len = normalizer.currency_patterns.len();
481    println!("  {currency_patterns_len} currency patterns");
482    let measurement_patterns_len = normalizer.measurement_patterns.len();
483    println!("  {measurement_patterns_len} measurement patterns");
484
485    // Test number normalization
486    println!("\n1. Number Normalization:");
487    println!("------------------------");
488    let number_tests = vec![
489        "I have twenty-three apples",
490        "The price is fifty dollars",
491        "Wait for thirty minutes",
492        "Buy two hundred shares",
493        "Population is one million",
494        "Temperature is zero degrees",
495    ];
496
497    for test in number_tests {
498        let normalized = normalizer.apply_normalizations(test);
499        println!("  '{test}' → '{normalized}'");
500    }
501
502    // Test ordinal normalization
503    println!("\n2. Ordinal Number Normalization:");
504    println!("--------------------------------");
505    let ordinal_tests = vec![
506        "This is the first time",
507        "Take the second exit",
508        "On the third floor",
509        "The twentieth century",
510        "Twenty-first birthday",
511    ];
512
513    for test in ordinal_tests {
514        let normalized = normalizer.apply_normalizations(test);
515        println!("  '{test}' → '{normalized}'");
516    }
517
518    // Test date normalization
519    println!("\n3. Date Normalization:");
520    println!("---------------------");
521    let date_tests = vec![
522        "Meeting on Jan 15, 2024",
523        "Born in February 3, 2023",
524        "Deadline is Mar 22, 2024",
525        "Holiday on 12/25/2023",
526        "Started on 01/01/2024",
527        "Due 3/15/24",
528    ];
529
530    for test in date_tests {
531        let normalized = normalizer.apply_normalizations(test);
532        println!("  '{test}' → '{normalized}'");
533    }
534
535    // Test time normalization
536    println!("\n4. Time Normalization:");
537    println!("---------------------");
538    let time_tests = vec![
539        "Meeting at 2:30 PM",
540        "Wake up at 6:15 AM",
541        "Lunch at noon",
542        "Deadline at midnight",
543        "Call at 3 o'clock",
544        "Due at quarter past three",
545    ];
546
547    for test in time_tests {
548        let normalized = normalizer.apply_normalizations(test);
549        println!("  '{test}' → '{normalized}'");
550    }
551
552    // Test currency normalization
553    println!("\n5. Currency Normalization:");
554    println!("-------------------------");
555    let currency_tests = vec![
556        "Cost is $25.50",
557        "Budget of ten dollars",
558        "Price €100 euros",
559        "Worth fifty pounds",
560        "Salary $1.5 million",
561        "Change fifty cents",
562    ];
563
564    for test in currency_tests {
565        let normalized = normalizer.apply_normalizations(test);
566        println!("  '{test}' → '{normalized}'");
567    }
568
569    // Test measurement normalization
570    println!("\n6. Measurement Normalization:");
571    println!("-----------------------------");
572    let measurement_tests = vec![
573        "Height 5 feet 10 inches",
574        "Distance 2 miles away",
575        "Weight 10 pounds",
576        "Volume 2 liters",
577        "Temperature 32 degrees Fahrenheit",
578        "Length 100 meters",
579    ];
580
581    for test in measurement_tests {
582        let normalized = normalizer.apply_normalizations(test);
583        println!("  '{test}' → '{normalized}'");
584    }
585
586    // Test phone number normalization
587    println!("\n7. Phone Number Normalization:");
588    println!("------------------------------");
589    let phone_tests = vec![
590        "Call (555) 123-4567",
591        "Text 555-123-4567",
592        "Fax 555.123.4567",
593        "Mobile 5551234567",
594        "Office +1 555 123 4567",
595    ];
596
597    for test in phone_tests {
598        let phone_results = normalize_phone_numbers(test);
599        if !phone_results.is_empty() {
600            let result = &phone_results[0];
601            println!(
602                "  '{}' → '{}'",
603                test,
604                test.replace(&result.original, &result.normalized)
605            );
606        } else {
607            println!("  '{test}' → {test} (no normalization)");
608        }
609    }
610
611    // Comprehensive text normalization
612    println!("\n8. Comprehensive Text Normalization:");
613    println!("------------------------------------");
614    let complex_texts = vec![
615        "The meeting is on Jan 15, 2024 at 2:30 PM with twenty-three people.",
616        "Budget: ten thousand dollars for the first quarter.",
617        "Temperature reached thirty-two degrees Fahrenheit at noon.",
618        "Flight duration: two hours and fifteen minutes on February 3, 2023.",
619        "Distance: five miles, weight: one hundred pounds, cost: $25.50.",
620    ];
621
622    for text in complex_texts {
623        let normalized = normalizer.apply_normalizations(text);
624        println!("\nOriginal:");
625        println!("  {text}");
626        println!("Normalized:");
627        println!("  {normalized}");
628
629        // Show detected entities
630        let entities = normalizer.normalize_text(text);
631        if !entities.is_empty() {
632            println!("Detected entities:");
633            for entity in entities {
634                println!(
635                    "  {:?}: '{}' → '{}'",
636                    entity.entity_type, entity.original, entity.normalized
637                );
638            }
639        }
640    }
641
642    // FST pipeline demonstration
643    println!("\n9. FST Pipeline Processing:");
644    println!("--------------------------");
645    println!("Demonstrating how FSTs can be used for normalization:");
646
647    let fst_test = "I need one apple, two oranges, and three bananas.";
648    let fst_result = process_with_fst_pipeline(fst_test, &_number_fst);
649    println!("  Input:  {fst_test}");
650    println!("  Output: {fst_result}");
651
652    // Applications and benefits
653    println!("\n10. Applications and Benefits:");
654    println!("-----------------------------");
655    println!("Number/Date normalization is essential for:");
656    println!("  • Text-to-Speech systems: consistent pronunciation");
657    println!("  • Search engines: matching different number formats");
658    println!("  • Data extraction: standardizing structured information");
659    println!("  • Machine translation: handling numerical expressions");
660    println!("  • Database integration: consistent data formats");
661    println!("  • Financial systems: standardizing currency amounts");
662    println!("  • Medical records: normalizing measurements and dosages");
663    println!("  • Legal documents: standardizing dates and references");
664
665    println!("\nFST advantages for normalization:");
666    println!("  • Bidirectional: normalization ↔ denormalization");
667    println!("  • Compositional: combine multiple normalization rules");
668    println!("  • Efficient: linear time processing");
669    println!("  • Deterministic: consistent results");
670    println!("  • Maintainable: rules are explicit and modifiable");
671    println!("  • Language-agnostic: same framework for different locales");
672
673    // Localization examples
674    println!("\n11. Localization Considerations:");
675    println!("--------------------------------");
676    println!("Different locales require different normalization rules:");
677    println!("  US: MM/DD/YYYY, $1,000.00, 5'10\"");
678    println!("  EU: DD/MM/YYYY, €1.000,00, 1.78m");
679    println!("  UK: DD/MM/YYYY, £1,000.00, 5ft 10in");
680    println!("  JP: YYYY/MM/DD, ¥1,000, 178cm");
681    println!();
682    println!("FSTs can easily handle locale-specific rules through:");
683    println!("  • Separate FSTs for each locale");
684    println!("  • Parameterized FST construction");
685    println!("  • Runtime rule switching");
686
687    Ok(())
688}