1use arcweight::prelude::*;
22
23#[derive(Debug, Clone, PartialEq)]
25enum NormalizationType {
26 Number,
27 Date,
28 Time,
29 Currency,
30 Measurement,
31 Ordinal,
32 PhoneNumber,
33}
34
35#[derive(Debug, Clone)]
37struct NormalizedEntity {
38 original: String,
39 normalized: String,
40 entity_type: NormalizationType,
41 _confidence: f32,
42}
43
44struct 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 ("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 ("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 ("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 ("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 ("thousand".to_string(), "1000".to_string()),
101 ("one thousand".to_string(), "1000".to_string()),
102 ("two thousand".to_string(), "2000".to_string()),
103 ("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 ("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 ("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 ("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: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 ("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 ("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 ("$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 ("€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 ("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 ("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 ("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 ("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 fn normalize_text(&self, text: &str) -> Vec<NormalizedEntity> {
241 let mut results = Vec::new();
242
243 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 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 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 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 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 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 fn apply_normalizations(&self, text: &str) -> String {
320 let mut result = text.to_string();
321
322 for (word, digit) in &self.word_to_digit {
324 result = result.replace(word, digit);
325 }
326
327 for (ordinal, number) in &self.ordinal_to_number {
329 result = result.replace(ordinal, number);
330 }
331
332 for (date_text, normalized_date) in &self.date_patterns {
334 result = result.replace(date_text, normalized_date);
335 }
336
337 for (time_text, normalized_time) in &self.time_patterns {
339 result = result.replace(time_text, normalized_time);
340 }
341
342 for (currency_text, normalized_currency) in &self.currency_patterns {
344 result = result.replace(currency_text, normalized_currency);
345 }
346
347 for (measurement_text, normalized_measurement) in &self.measurement_patterns {
349 result = result.replace(measurement_text, normalized_measurement);
350 }
351
352 result
353 }
354}
355
356fn 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 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 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, TropicalWeight::one(),
384 next,
385 ),
386 );
387 current = next;
388 }
389
390 for ch in digit.chars() {
392 let next = fst.add_state();
393 fst.add_arc(
394 current,
395 Arc::new(
396 0, ch as u32,
398 TropicalWeight::one(),
399 next,
400 ),
401 );
402 current = next;
403 }
404
405 fst.add_arc(
407 current,
408 Arc::new(
409 0, 0, TropicalWeight::one(),
412 start,
413 ),
414 );
415 }
416
417 fst
418}
419
420fn 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
445fn process_with_fst_pipeline(text: &str, _normalizer_fst: &VectorFst<TropicalWeight>) -> String {
447 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 let normalizer = NumberNormalizer::new();
467
468 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 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 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 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 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 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 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 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 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 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 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 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 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}