duckling 0.4.0

A Rust port of Facebook's Duckling library for parsing natural language into structured data
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
use super::{PartOfDay, TimeData, TimeForm};
use crate::dimensions::time_grain::Grain;
use crate::pattern::regex;
use crate::types::{Rule, TokenData};

fn parse_zh_num(s: &str) -> Option<u32> {
    let s = s.trim();
    if s.is_empty() {
        return None;
    }
    if let Ok(v) = s.parse::<u32>() {
        return Some(v);
    }
    fn d(c: char) -> Option<u32> {
        match c {
            '' | '' => Some(0),
            '' => Some(1),
            '' | '' | '' => Some(2),
            '' => Some(3),
            '' => Some(4),
            '' => Some(5),
            '' => Some(6),
            '' => Some(7),
            '' => Some(8),
            '' => Some(9),
            _ => None,
        }
    }
    if s == "" {
        return Some(10);
    }
    if s == "廿" {
        return Some(20);
    }
    if s == "" {
        return Some(30);
    }
    if let Some(rest) = s.strip_prefix('廿') {
        let tail = if rest.is_empty() {
            0
        } else {
            d(rest.chars().next()?)?
        };
        return 20_u32.checked_add(tail);
    }
    if let Some(rest) = s.strip_prefix('') {
        let tail = if rest.is_empty() {
            0
        } else {
            d(rest.chars().next()?)?
        };
        return 30_u32.checked_add(tail);
    }
    if let Some(rest) = s.strip_prefix('') {
        return d(rest.chars().next()?)?.checked_add(10);
    }
    if let Some(rest) = s.strip_suffix('') {
        return d(rest.chars().next()?)?.checked_mul(10);
    }
    if let Some((a, b)) = s.split_once('') {
        let aa = if a.is_empty() {
            1
        } else {
            d(a.chars().next()?)?
        };
        let bb = if b.is_empty() {
            0
        } else {
            d(b.chars().next()?)?
        };
        return aa.checked_mul(10)?.checked_add(bb);
    }
    d(s.chars().next()?)
}

fn parse_zh_grain(s: &str) -> Option<Grain> {
    if s.starts_with("") {
        return Some(Grain::Second);
    }
    if s.starts_with("分鐘") || s.starts_with("分钟") || s.starts_with("") {
        return Some(Grain::Minute);
    }
    if s.starts_with("小時")
        || s.starts_with("小时")
        || s.starts_with("個鐘")
        || s.starts_with("")
        || s.starts_with("")
    {
        return Some(Grain::Hour);
    }
    if s == "" || s == "" {
        return Some(Grain::Day);
    }
    if s == "" || s == "" || s == "礼拜" || s == "禮拜" || s == "星期" {
        return Some(Grain::Week);
    }
    if s == "" {
        return Some(Grain::Month);
    }
    if s == "" {
        return Some(Grain::Year);
    }
    None
}

fn fixed_holiday_rule(name: &str, pattern: &str, month: u32, day: u32) -> Rule {
    Rule {
        name: name.to_string(),
        pattern: vec![regex(pattern)],
        production: Box::new(move |_| {
            Some(TokenData::Time(TimeData::new(TimeForm::DateMDY {
                month,
                day,
                year: None,
            })))
        }),
    }
}

pub fn rules() -> Vec<Rule> {
    let mut rules = super::en::rules();
    rules.extend(vec![
        Rule { name: "now (zh)".to_string(), pattern: vec![regex("现在|現在|此时|此時|当前|當前|宜家|而家|依家")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Now)))) },
        Rule { name: "today (zh)".to_string(), pattern: vec![regex("今天|今日|此刻")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Today)))) },
        Rule { name: "tomorrow (zh)".to_string(), pattern: vec![regex("明天|明日|聽日")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Tomorrow)))) },
        Rule { name: "day after tomorrow (zh)".to_string(), pattern: vec![regex("后天|後天|後日")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::DayAfterTomorrow)))) },
        Rule { name: "day before yesterday (zh)".to_string(), pattern: vec![regex("前天|前日")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::DayBeforeYesterday)))) },
        Rule { name: "morning (zh)".to_string(), pattern: vec![regex("早上|早晨|朝早")], production: Box::new(|_| Some(TokenData::Time(TimeData::latent(TimeForm::PartOfDay(PartOfDay::Morning))))) },
        Rule { name: "afternoon (zh)".to_string(), pattern: vec![regex("下午|午后|午後")], production: Box::new(|_| Some(TokenData::Time(TimeData::latent(TimeForm::PartOfDay(PartOfDay::Afternoon))))) },
        Rule { name: "evening (zh)".to_string(), pattern: vec![regex("晚上|晚间|晚間")], production: Box::new(|_| Some(TokenData::Time(TimeData::latent(TimeForm::PartOfDay(PartOfDay::Evening))))) },
        Rule { name: "night (zh)".to_string(), pattern: vec![regex("夜晚|夜里|夜裡|夜间|夜間")], production: Box::new(|_| Some(TokenData::Time(TimeData::latent(TimeForm::PartOfDay(PartOfDay::Night))))) },
        Rule { name: "weekday monday (zh)".to_string(), pattern: vec![regex("星期一|週一|周一|礼拜一|禮拜一")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::DayOfWeek(0))))) },
        Rule { name: "weekday tuesday (zh)".to_string(), pattern: vec![regex("星期二|週二|周二|礼拜二|禮拜二")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::DayOfWeek(1))))) },
        Rule { name: "weekday wednesday (zh)".to_string(), pattern: vec![regex("星期三|週三|周三|礼拜三|禮拜三")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::DayOfWeek(2))))) },
        Rule { name: "weekday thursday (zh)".to_string(), pattern: vec![regex("星期四|週四|周四|礼拜四|禮拜四")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::DayOfWeek(3))))) },
        Rule { name: "weekday friday (zh)".to_string(), pattern: vec![regex("星期五|週五|周五|礼拜五|禮拜五")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::DayOfWeek(4))))) },
        Rule { name: "weekday saturday (zh)".to_string(), pattern: vec![regex("星期六|週六|周六|礼拜六|禮拜六")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::DayOfWeek(5))))) },
        Rule { name: "weekday sunday (zh)".to_string(), pattern: vec![regex("星期日|週日|周日|星期天|週天|周天|礼拜天|禮拜天")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::DayOfWeek(6))))) },
        Rule { name: "this weekend (zh)".to_string(), pattern: vec![regex("这周末|這週末")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Weekend)))) },
        Rule { name: "this week (zh)".to_string(), pattern: vec![regex("这周|這週|这一周|這一周|這一週|本周|本週")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::AllGrain(Grain::Week))))) },
        Rule { name: "this week alt (zh)".to_string(), pattern: vec![regex("这礼拜|這禮拜|这一礼拜|這一禮拜|这个礼拜|這個禮拜")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::AllGrain(Grain::Week))))) },
        Rule { name: "this week cantonese (zh)".to_string(), pattern: vec![regex("今個星期|今个星期|今個禮拜|今個礼拜|今个礼拜")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::AllGrain(Grain::Week))))) },
        Rule { name: "last week (zh)".to_string(), pattern: vec![regex("上周|上週|上星期|上礼拜|上禮拜|上一周|上一週")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::GrainOffset { grain: Grain::Week, offset: -1 })))) },
        Rule { name: "last week cantonese (zh)".to_string(), pattern: vec![regex("上個星期|上个星期|上個禮拜|上個礼拜|上个礼拜")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::GrainOffset { grain: Grain::Week, offset: -1 })))) },
        Rule { name: "next week (zh)".to_string(), pattern: vec![regex("下周|下週|下星期|下礼拜|下禮拜|下一周|下一週")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::GrainOffset { grain: Grain::Week, offset: 1 })))) },
        Rule { name: "next week cantonese (zh)".to_string(), pattern: vec![regex("下個星期|下个星期|下個禮拜|下個礼拜|下个礼拜")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::GrainOffset { grain: Grain::Week, offset: 1 })))) },
        Rule { name: "this month (zh)".to_string(), pattern: vec![regex("本月|这个月|這個月|今月")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::AllGrain(Grain::Month))))) },
        Rule { name: "last month (zh)".to_string(), pattern: vec![regex("上月|上个月|上個月")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::GrainOffset { grain: Grain::Month, offset: -1 })))) },
        Rule { name: "next month (zh)".to_string(), pattern: vec![regex("下月|下个月|下個月")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::GrainOffset { grain: Grain::Month, offset: 1 })))) },
        Rule { name: "last year (zh)".to_string(), pattern: vec![regex("去年|上年")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::GrainOffset { grain: Grain::Year, offset: -1 })))) },
        Rule { name: "this year (zh)".to_string(), pattern: vec![regex("今年|这一年|這一年|这年|這年|今個年|今个年")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::AllGrain(Grain::Year))))) },
        Rule { name: "next year (zh)".to_string(), pattern: vec![regex("明年|下年")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::GrainOffset { grain: Grain::Year, offset: 1 })))) },
        Rule {
            name: "last n cycle (zh)".to_string(),
            pattern: vec![regex("(上|前)([一二三四五六七八九十兩两廿卅\\d]{1,4})(?:个|個)?(秒(钟|鐘)?|分鐘?|分钟|小時|小时|個鐘|鐘頭?|钟头?|天|日|周|週|礼拜|禮拜|星期|月|年)")],
            production: Box::new(|nodes| {
                let (n, gtxt) = match &nodes[0].token_data {
                    TokenData::RegexMatch(rm) => (rm.group(2)?, rm.group(3)?),
                    _ => return None,
                };
                let n = parse_zh_num(n)? as i64;
                let grain = parse_zh_grain(gtxt)?;
                Some(TokenData::Time(TimeData::new(TimeForm::NthGrain {
                    n,
                    grain,
                    past: true,
                    interval: true,
                })))
            }),
        },
        Rule {
            name: "n cycle last (zh)".to_string(),
            pattern: vec![regex("([一二三四五六七八九十兩两廿卅\\d]{1,4})(?:个|個)?(秒(钟|鐘)?|分鐘?|分钟|小時|小时|個鐘|鐘頭?|钟头?|天|日|周|週|礼拜|禮拜|星期|月|年)(之)?前")],
            production: Box::new(|nodes| {
                let (n, gtxt) = match &nodes[0].token_data {
                    TokenData::RegexMatch(rm) => (rm.group(1)?, rm.group(2)?),
                    _ => return None,
                };
                let n = parse_zh_num(n)? as i64;
                let grain = parse_zh_grain(gtxt)?;
                Some(TokenData::Time(TimeData::new(TimeForm::NthGrain {
                    n,
                    grain,
                    past: true,
                    interval: true,
                })))
            }),
        },
        Rule {
            name: "next n cycle (zh)".to_string(),
            pattern: vec![regex("(下|后|後)([一二三四五六七八九十兩两廿卅\\d]{1,4})(?:个|個)?(秒(钟|鐘)?|分鐘?|分钟|小時|小时|個鐘|鐘頭?|钟头?|天|日|周|週|礼拜|禮拜|星期|月|年)")],
            production: Box::new(|nodes| {
                let (n, gtxt) = match &nodes[0].token_data {
                    TokenData::RegexMatch(rm) => (rm.group(2)?, rm.group(3)?),
                    _ => return None,
                };
                let n = parse_zh_num(n)? as i64;
                let grain = parse_zh_grain(gtxt)?;
                Some(TokenData::Time(TimeData::new(TimeForm::NthGrain {
                    n,
                    grain,
                    past: false,
                    interval: true,
                })))
            }),
        },
        Rule {
            name: "n cycle next (zh)".to_string(),
            pattern: vec![regex("([一二三四五六七八九十兩两廿卅\\d]{1,4})(?:个|個)?(秒(钟|鐘)?|分鐘?|分钟|小時|小时|個鐘|鐘頭?|钟头?|天|日|周|週|礼拜|禮拜|星期|月|年)(之)?(后|後)")],
            production: Box::new(|nodes| {
                let (n, gtxt) = match &nodes[0].token_data {
                    TokenData::RegexMatch(rm) => (rm.group(1)?, rm.group(2)?),
                    _ => return None,
                };
                let n = parse_zh_num(n)? as i64;
                let grain = parse_zh_grain(gtxt)?;
                Some(TokenData::Time(TimeData::new(TimeForm::NthGrain {
                    n,
                    grain,
                    past: false,
                    interval: true,
                })))
            }),
        },
        Rule { name: "last sunday (zh)".to_string(), pattern: vec![regex("上禮拜日|上礼拜日|上週日|上周日")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::DayOfWeek(6))))) },
        Rule {
            name: "month-day chinese numerals (zh)".to_string(),
            pattern: vec![regex("([一二三四五六七八九十兩两廿卅\\d]{1,3})月([一二三四五六七八九十兩两廿卅\\d]{1,3})(日|号|號)")],
            production: Box::new(|nodes| {
                let (m, d) = match &nodes[0].token_data {
                    TokenData::RegexMatch(rm) => (rm.group(1)?, rm.group(2)?),
                    _ => return None,
                };
                let month = parse_zh_num(m)?;
                let day = parse_zh_num(d)?;
                if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
                    return None;
                }
                Some(TokenData::Time(TimeData::new(TimeForm::DateMDY { month, day, year: None })))
            }),
        },
        Rule {
            name: "month only (zh)".to_string(),
            pattern: vec![regex("([一二三四五六七八九十兩两廿卅\\d]{1,3})月")],
            production: Box::new(|nodes| {
                let m = match &nodes[0].token_data {
                    TokenData::RegexMatch(rm) => rm.group(1)?,
                    _ => return None,
                };
                let month = parse_zh_num(m)?;
                if !(1..=12).contains(&month) {
                    return None;
                }
                Some(TokenData::Time(TimeData::new(TimeForm::Month(month))))
            }),
        },
        Rule {
            name: "weekday comma month-day (zh)".to_string(),
            pattern: vec![regex("(星期|週|周|礼拜|禮拜)[一二三四五六日天],?\\s*([一二三四五六七八九十兩两廿卅\\d]{1,3})月([一二三四五六七八九十兩两廿卅\\d]{1,3})(日|号|號)")],
            production: Box::new(|nodes| {
                let (m, d) = match &nodes[0].token_data {
                    TokenData::RegexMatch(rm) => (rm.group(2)?, rm.group(3)?),
                    _ => return None,
                };
                let month = parse_zh_num(m)?;
                let day = parse_zh_num(d)?;
                if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
                    return None;
                }
                Some(TokenData::Time(TimeData::new(TimeForm::DateMDY { month, day, year: None })))
            }),
        },
        Rule {
            name: "relative month + day (zh)".to_string(),
            pattern: vec![regex("(本月|这个月|這個月|今月|今個月|今个月|上月|上个月|上個月|下月|下个月|下個月)([一二三四五六七八九十兩两廿卅\\d]{1,3})(日|号|號)")],
            production: Box::new(|nodes| {
                let (prefix, d) = match &nodes[0].token_data {
                    TokenData::RegexMatch(rm) => (rm.group(1)?, rm.group(2)?),
                    _ => return None,
                };
                let day = parse_zh_num(d)?;
                if !(1..=31).contains(&day) {
                    return None;
                }
                let month_base = if matches!(prefix, "上月" | "上个月" | "上個月") {
                    TimeForm::GrainOffset { grain: Grain::Month, offset: -1 }
                } else if matches!(prefix, "下月" | "下个月" | "下個月") {
                    TimeForm::GrainOffset { grain: Grain::Month, offset: 1 }
                } else {
                    TimeForm::AllGrain(Grain::Month)
                };
                Some(TokenData::Time(TimeData::new(TimeForm::Composed(
                    Box::new(TimeData::new(month_base)),
                    Box::new(TimeData::new(TimeForm::DayOfMonth(day))),
                ))))
            }),
        },
        Rule { name: "yesterday (zh)".to_string(), pattern: vec![regex("昨天|昨日|尋日")], production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Yesterday)))) },
        Rule {
            name: "hour-of-day (zh)".to_string(),
            pattern: vec![regex("([零〇一二三四五六七八九十兩两廿卅\\d]{1,3})(点|點|時)")],
            production: Box::new(|nodes| {
                let h = match &nodes[0].token_data {
                    TokenData::RegexMatch(rm) => rm.group(1)?,
                    _ => return None,
                };
                let hour = parse_zh_num(h)?;
                if hour > 23 {
                    return None;
                }
                Some(TokenData::Time(TimeData::new(TimeForm::Hour(hour, true))))
            }),
        },
        Rule {
            name: "hh:mm (zh)".to_string(),
            pattern: vec![regex("((?:[01]?\\d)|(?:2[0-3]))[::]([0-5]\\d)")],
            production: Box::new(|nodes| {
                let (h, m) = match &nodes[0].token_data {
                    TokenData::RegexMatch(rm) => (rm.group(1)?, rm.group(2)?),
                    _ => return None,
                };
                let hour = h.parse::<u32>().ok()?;
                let minute = m.parse::<u32>().ok()?;
                Some(TokenData::Time(TimeData::new(TimeForm::HourMinute(hour, minute, true))))
            }),
        },
        Rule {
            name: "guoqing (zh)".to_string(),
            pattern: vec![regex("国庆|國慶")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::DateMDY { month: 10, day: 1, year: None })))),
        },
        Rule {
            name: "new year day (zh)".to_string(),
            pattern: vec![regex("元旦(节|節)?|((公|(阳|陽))(历|曆))?新年")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("new year".to_string(), None))))),
        },
        fixed_holiday_rule("women day (zh)", "(国际劳动|國際勞動|三八)?(妇|婦)女(节|節)", 3, 8),
        fixed_holiday_rule("labor day (zh)", "(五一|51)?(国际|國際)?(劳动|勞動)(节|節)", 5, 1),
        fixed_holiday_rule("youth day (zh)", "(中(国|國))?(五四|54)?青年(节|節)", 5, 4),
        fixed_holiday_rule("children day (zh)", "(国际|國際)?(六一|61)?(儿|兒)童(节|節)", 6, 1),
        fixed_holiday_rule("teachers day (zh)", "(中(国|國))?教师(节|節)", 9, 10),
        fixed_holiday_rule("halloween (zh)", "万圣节|萬聖節", 10, 31),
        fixed_holiday_rule("singles day (zh)", "光棍(节|節)|(双|雙)(十一|11)", 11, 11),
        fixed_holiday_rule("christmas eve (zh)", "(平安|聖誕)夜", 12, 24),
        fixed_holiday_rule("christmas day (zh)", "(圣诞|聖誕)(节|節)?", 12, 25),
        fixed_holiday_rule("new year eve (zh)", "新年夜", 12, 31),
        fixed_holiday_rule("valentine day (zh)", "(情人|(圣瓦伦丁|聖瓦倫丁))(节|節)", 2, 14),
        fixed_holiday_rule("white day (zh)", "白色情人(节|節)", 3, 14),
        fixed_holiday_rule("fools day (zh)", "愚人(节|節)", 4, 1),
        fixed_holiday_rule("qingming (zh)", "清明(节|節)", 4, 5),
        fixed_holiday_rule("consumer rights day (zh)", "(国际|世界)?(消费者权益|消費者權益)日|三一五", 3, 15),
        fixed_holiday_rule("world environment day (zh)", "世界(环|環)境日", 6, 5),
        fixed_holiday_rule("world ocean day (zh)", "世界海洋日", 6, 8),
        fixed_holiday_rule("world blood donor day (zh)", "世界(献|獻)血日", 6, 14),
        fixed_holiday_rule("world refugee day (zh)", "世界(难|難)民日", 6, 20),
        fixed_holiday_rule("world yoga day (zh)", "(国际|國際)瑜伽日", 6, 21),
        fixed_holiday_rule("world olympic day (zh)", "(国际|國際)奥林匹克日", 6, 23),
        fixed_holiday_rule("us independence day (zh)", "(美国)?(独|獨)立日", 7, 4),
        fixed_holiday_rule("army day (zh)", "(中国人民解放(军|軍)|八一)?建(军节|軍節)", 8, 1),
        fixed_holiday_rule("intl youth day (zh)", "(国际|國際)青年(节|節)", 8, 12),
        fixed_holiday_rule("xinhai memorial (zh)", "辛亥革命(纪|紀)念日", 10, 10),
        fixed_holiday_rule("world food day (zh)", "世界((粮|糧)食|食物)日", 10, 16),
        fixed_holiday_rule("veterans day (zh)", "(退伍(军|軍)人|老兵)(节|節)", 11, 11),
        fixed_holiday_rule("world diabetes day (zh)", "世界糖尿病日", 11, 14),
        fixed_holiday_rule("human rights day (zh)", "人(权|權)日", 12, 10),
        fixed_holiday_rule("nanjing memorial day (zh)", "南京大屠(杀纪|殺紀)念日", 12, 13),
        fixed_holiday_rule("macao handover day (zh)", "澳(门|門)回(归纪|歸紀)念日", 12, 20),
        Rule {
            name: "chinese new year (zh)".to_string(),
            pattern: vec![regex("春(节|節)|(农历|農曆|唐人)新年|新(正|春)|正月(正(时|時)|朔日)|岁首")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("chinese new year".to_string(), None))))),
        },
        Rule {
            name: "easter (zh)".to_string(),
            pattern: vec![regex("(复|復)活(节|節)|主(复|復)活日")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("easter".to_string(), None))))),
        },
        Rule {
            name: "easter monday (zh)".to_string(),
            pattern: vec![regex("(复|復)活(节|節)星期一")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("easter monday".to_string(), None))))),
        },
        Rule {
            name: "good friday (zh)".to_string(),
            pattern: vec![regex("(耶(稣|穌)|主)受(难|難)(节|節|日)|(圣|聖|沈默)(周|週)五")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("good friday".to_string(), None))))),
        },
        Rule {
            name: "ash wednesday (zh)".to_string(),
            pattern: vec![regex("大(斋|齋)首日|(圣|聖)灰((礼仪|禮儀)?日|星期三)|灰日")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("ash wednesday".to_string(), None))))),
        },
        Rule {
            name: "pentecost (zh)".to_string(),
            pattern: vec![regex("五旬(节|節)|(圣灵|聖靈)降(临|臨)(日|节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("pentecost".to_string(), None))))),
        },
        Rule {
            name: "trinity sunday (zh)".to_string(),
            pattern: vec![regex("((天主)?(圣|聖)?三一|(圣|聖)三)(主日|节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("trinity sunday".to_string(), None))))),
        },
        Rule {
            name: "palm sunday (zh)".to_string(),
            pattern: vec![regex("((棕|圣|聖)枝|圣树|聖樹|基督苦(难|難))主日")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("palm sunday".to_string(), None))))),
        },
        Rule {
            name: "yom kippur (zh)".to_string(),
            pattern: vec![regex("(赎|贖)罪日")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("yom kippur".to_string(), None))))),
        },
        Rule {
            name: "eid al-fitr (zh)".to_string(),
            pattern: vec![regex("(开斋|開齋|肉孜|(尔|爾)代)(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("eid al-fitr".to_string(), None))))),
        },
        Rule {
            name: "eid al-adha (zh)".to_string(),
            pattern: vec![regex("古(尔|爾)邦(节|節)|宰牲(节|節)|难近母(节|節)|難近母(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("eid al-adha".to_string(), None))))),
        },
        Rule {
            name: "mawlid (zh)".to_string(),
            pattern: vec![regex("圣纪节|聖紀節")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("mawlid".to_string(), None))))),
        },
        Rule {
            name: "islamic new year (zh)".to_string(),
            pattern: vec![regex("伊斯兰(教)?(历)?新年")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("islamic new year".to_string(), None))))),
        },
        Rule {
            name: "ashura (zh)".to_string(),
            pattern: vec![regex("阿舒拉(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("ashura".to_string(), None))))),
        },
        Rule {
            name: "chhath (zh)".to_string(),
            pattern: vec![regex("克哈特普迦(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("chhath".to_string(), None))))),
        },
        Rule {
            name: "diwali (zh)".to_string(),
            pattern: vec![regex("(排|万|萬|印度)(灯节|燈節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("diwali".to_string(), None))))),
        },
        Rule {
            name: "holi (zh)".to_string(),
            pattern: vec![regex("((侯|荷)(丽|麗)|洒红|灑紅|欢悦|歡悅|五彩|胡里|好利|霍利)(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("holi".to_string(), None))))),
        },
        Rule {
            name: "dussehra (zh)".to_string(),
            pattern: vec![regex("(十(胜|勝)|(凯|凱)旋|(圣|聖)母)(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("dussehra".to_string(), None))))),
        },
        Rule {
            name: "onam (zh)".to_string(),
            pattern: vec![regex("欧南(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("onam".to_string(), None))))),
        },
        Rule {
            name: "raksha bandhan (zh)".to_string(),
            pattern: vec![regex("(印度兄妹|拉克沙班丹)(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("raksha bandhan".to_string(), None))))),
        },
        Rule {
            name: "vesak (zh)".to_string(),
            pattern: vec![regex("((卫|衛)塞|威瑟|比(萨宝|薩寶)蕉)(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("vesak".to_string(), None))))),
        },
        Rule {
            name: "tisha b'av (zh)".to_string(),
            pattern: vec![regex("((圣|聖)殿被毁|禁食)日")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("tisha b'av".to_string(), None))))),
        },
        Rule {
            name: "tu bishvat (zh)".to_string(),
            pattern: vec![regex("((犹|猶)太植(树|樹)|(图|圖)比舍巴特)(节|節)|(树|樹)木新年")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("tu bishvat".to_string(), None))))),
        },
        Rule {
            name: "ascension day (zh)".to_string(),
            pattern: vec![regex("耶(稣|穌)升天(节|節|日)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("ascension day".to_string(), None))))),
        },
        Rule {
            name: "corpus christi (zh)".to_string(),
            pattern: vec![regex("基督(圣体|聖體)((圣|聖)血)?((节|節)|瞻(礼|禮))")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("corpus christi".to_string(), None))))),
        },
        Rule {
            name: "holy saturday (zh)".to_string(),
            pattern: vec![regex("神?(圣周|聖週)六|(耶(稣|穌)|主)受(难|難)(节|節|日)翌日|(复|復)活(节|節)前夜|黑色星期六")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("holy saturday".to_string(), None))))),
        },
        Rule {
            name: "orthodox easter monday (zh)".to_string(),
            pattern: vec![regex("(东|東)正教(复|復)活(节|節)星期一")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("orthodox easter monday".to_string(), None))))),
        },
        Rule {
            name: "orthodox easter (zh)".to_string(),
            pattern: vec![regex("(东|東)正教((复|復)活(节|節)|主(复|復)活日)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("orthodox easter".to_string(), None))))),
        },
        Rule {
            name: "orthodox holy saturday (zh)".to_string(),
            pattern: vec![regex("(东|東)正教(神?(圣周|聖週)六|(耶(稣|穌)|主)受(难|難)(节|節|日)翌日|(复|復)活(节|節)前夜)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("orthodox holy saturday".to_string(), None))))),
        },
        Rule {
            name: "orthodox good friday (zh)".to_string(),
            pattern: vec![regex("(东|東)正教((耶(稣|穌)|主)受(难|難)(节|節|日)|(圣|聖|沈默)(周|週)五)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("orthodox good friday".to_string(), None))))),
        },
        Rule {
            name: "ascension to heaven (zh)".to_string(),
            pattern: vec![regex("(夜行)?登霄(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("ascension to heaven".to_string(), None))))),
        },
        Rule {
            name: "thai pongal (zh)".to_string(),
            pattern: vec![regex("(印度|淡米(尔|爾))(丰|豐)收(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("thai pongal".to_string(), None))))),
        },
        Rule {
            name: "maattu pongal (zh)".to_string(),
            pattern: vec![regex("(印度(丰|豐)收|(庞|龐)格(尔|爾))(节|節)第三天")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("maattu pongal".to_string(), None))))),
        },
        Rule {
            name: "kaanum pongal (zh)".to_string(),
            pattern: vec![regex("(印度(丰|豐)收|(庞|龐)格(尔|爾))(节|節)第四天")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("kaanum pongal".to_string(), None))))),
        },
        Rule {
            name: "lag baomer (zh)".to_string(),
            pattern: vec![regex("((犹|猶)太教)?篝火(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("lag baomer".to_string(), None))))),
        },
        Rule {
            name: "night of decree (zh)".to_string(),
            pattern: vec![regex("(法令|命运|权力)之夜")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("night of decree".to_string(), None))))),
        },
        Rule {
            name: "maundy thursday (zh)".to_string(),
            pattern: vec![regex("濯足(节|節)|神(圣|聖)星期四|(圣周|聖週)(星期)?四|(设|設)立(圣|聖)餐日")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("maundy thursday".to_string(), None))))),
        },
        Rule {
            name: "shemini atzeret (zh)".to_string(),
            pattern: vec![regex("(圣|聖)会(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("shemini atzeret".to_string(), None))))),
        },
        Rule {
            name: "simchat torah (zh)".to_string(),
            pattern: vec![regex("(西赫(托拉|妥拉)|诵经|誦經|转经|轉經|律法|(欢庆圣|歡慶聖)法)(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("simchat torah".to_string(), None))))),
        },
        Rule {
            name: "mardi gras (zh)".to_string(),
            pattern: vec![regex("忏悔(节|節|火曜日)|煎(饼|餅)星期二")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("mardi gras".to_string(), None))))),
        },
        Rule {
            name: "monday of holy spirit (zh)".to_string(),
            pattern: vec![regex("(圣灵节庆|聖靈節慶)日")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("monday of the holy spirit".to_string(), None))))),
        },
        Rule {
            name: "boss day (zh)".to_string(),
            pattern: vec![regex("老(板节|闆節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("boss day".to_string(), None))))),
        },
        Rule {
            name: "global youth service day (zh)".to_string(),
            pattern: vec![regex("全球青年服(务|務)日")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("global youth service day".to_string(), None))))),
        },
        Rule {
            name: "great fast (zh)".to_string(),
            pattern: vec![regex("四旬(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("great fast".to_string(), None))))),
        },
        Rule {
            name: "chanukah (zh)".to_string(),
            pattern: vec![regex("(光明|修殿|(献|獻)殿|(烛|燭)光|哈努卡|(马|馬)加比)(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("chanukah".to_string(), None))))),
        },
        Rule {
            name: "lent (zh)".to_string(),
            pattern: vec![regex("大(斋|齋)(期|节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("lent".to_string(), None))))),
        },
        Rule {
            name: "navaratri (zh)".to_string(),
            pattern: vec![regex("(九夜|那瓦拉提)(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("navaratri".to_string(), None))))),
        },
        Rule {
            name: "passover (zh)".to_string(),
            pattern: vec![regex("逾越(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("passover".to_string(), None))))),
        },
        Rule {
            name: "ramadan (zh)".to_string(),
            pattern: vec![regex("斋月|齋月|斋戒月|齋戒月|莱麦丹|萊麥丹|拉马丹|拉馬丹")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("ramadan".to_string(), None))))),
        },
        Rule {
            name: "rosh hashanah (zh)".to_string(),
            pattern: vec![regex("(犹|猶)太新年|吹角(节|節)|(犹太)?新年(节|節)|(犹|猶)太历新年")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("rosh hashanah".to_string(), None))))),
        },
        Rule {
            name: "shavuot (zh)".to_string(),
            pattern: vec![regex("(五旬|七七|收获|收穫)(节|節)|圣灵降临节|聖靈降臨節|沙夫幼特(节|節)|新果实(节|節)|新果實(节|節)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("shavuot".to_string(), None))))),
        },
        Rule {
            name: "sukkot (zh)".to_string(),
            pattern: vec![regex("(住棚|帐棚|帳棚|结茅|結茅)(节|節)|住棚(节|節)?|苏科特")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("sukkot".to_string(), None))))),
        },
        Rule {
            name: "earth hour (zh)".to_string(),
            pattern: vec![regex("地球一小(时|時)")],
            production: Box::new(|_| Some(TokenData::Time(TimeData::new(TimeForm::Holiday("earth hour".to_string(), None))))),
        },
    ]);
    rules
}