Coverage Report

Created: 2025-08-13 21:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/a220/proj/radnelac/src/display/private.rs
Line
Count
Source
1
// This Source Code Form is subject to the terms of the Mozilla Public
2
// License, v. 2.0. If a copy of the MPL was not distributed with this
3
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5
use crate::calendar::CommonDate;
6
use crate::calendar::OrdinalDate;
7
use crate::calendar::Quarter;
8
use crate::day_count::BoundedDayCount;
9
use crate::day_count::Epoch;
10
use crate::day_count::ToFixed;
11
use crate::display::text::en::EN_DICTIONARY;
12
use crate::display::text::fr::FR_DICTIONARY;
13
use crate::display::text::prelude::Dictionary;
14
use crate::display::text::prelude::Language;
15
use convert_case;
16
use convert_case::Casing;
17
use num_traits::NumAssign;
18
use num_traits::Signed;
19
use num_traits::ToPrimitive;
20
use numerals::roman::Roman;
21
use std::cmp::max;
22
23
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
24
pub enum NumericContent {
25
    Month,
26
    DayOfWeek,
27
    DayOfMonth,
28
    DayOfYear,
29
    Hour1to12,
30
    Hour0to23,
31
    Minute,
32
    Second,
33
    SecondsSinceEpoch,
34
    Year,
35
    Quarter,
36
    DaysSinceEpoch,
37
    ComplementaryDay,
38
    WeekOfYear,
39
}
40
41
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
42
pub enum TextContent {
43
    MonthName,
44
    DayOfMonthName,
45
    DayOfWeekName,
46
    HalfDayName,
47
    HalfDayAbbrev,
48
    EraName,
49
    EraAbbreviation,
50
    ComplementaryDayName,
51
}
52
53
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
54
pub enum Content<'a> {
55
    Literal(&'a str),
56
    Numeric(NumericContent),
57
    Text(TextContent),
58
}
59
60
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
61
pub enum Align {
62
    Left,
63
    Center,
64
    Right,
65
}
66
67
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
68
pub enum Case {
69
    Upper,
70
    Lower,
71
    Title,
72
}
73
74
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
75
pub enum Sign {
76
    Always,
77
    OnlyNegative,
78
    Never,
79
}
80
81
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
82
pub enum Numerals {
83
    HinduArabic,
84
    Roman,
85
}
86
87
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
88
pub struct DisplayOptions {
89
    pub width: Option<usize>,
90
    pub align: Option<Align>,
91
    pub case: Option<Case>,
92
    pub padding: Option<char>,
93
    pub numerals: Option<Numerals>,
94
    pub sign: Sign,
95
}
96
97
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
98
pub struct Item<'a> {
99
    pub content: Content<'a>,
100
    pub options: DisplayOptions,
101
}
102
103
impl<'a> Item<'a> {
104
1.02k
    pub const fn new(content: Content<'a>, options: DisplayOptions) -> Self {
105
1.02k
        Item {
106
1.02k
            content: content,
107
1.02k
            options: options,
108
1.02k
        }
109
1.02k
    }
110
}
111
112
pub trait DisplayItem {
113
    fn supported_lang(lang: Language) -> bool;
114
    fn fmt_numeric(&self, n: NumericContent, opt: DisplayOptions) -> String;
115
    fn fmt_text(&self, t: TextContent, lang: Language, opt: DisplayOptions) -> String;
116
117
971k
    fn fmt_item(&self, lang: Language, item: Item) -> String {
118
971k
        match item.content {
119
399k
            Content::Literal(s) => String::from(s),
120
368k
            Content::Numeric(n) => self.fmt_numeric(n, item.options),
121
203k
            Content::Text(t) => self.fmt_text(t, lang, item.options),
122
        }
123
971k
    }
124
}
125
126
567k
pub fn fmt_string(root: &str, opt: DisplayOptions) -> String {
127
567k
    let mut result = String::new();
128
567k
    let cased_root = if opt.case.is_some() {
129
7.21k
        let case = match opt.case.unwrap() {
130
7.21k
            Case::Upper => convert_case::Case::UpperFlat,
131
1
            Case::Lower => convert_case::Case::Flat,
132
1
            Case::Title => convert_case::Case::UpperCamel,
133
        };
134
7.21k
        root.to_case(case)
135
    } else {
136
560k
        String::from(root)
137
    };
138
139
567k
    if opt.width.is_some() && 
opt.width225k
.
unwrap225k
() > cased_root.len() {
140
13
        let align = opt.align.unwrap_or(Align::Left);
141
13
        let pad_char = opt.padding.unwrap_or(' ');
142
13
        let pad_width = opt.width.unwrap() - cased_root.len();
143
13
        let pad_left = std::iter::repeat(pad_char)
144
13
            .take((pad_width / 2) + (pad_width % 2))
145
13
            .collect::<String>();
146
13
        let pad_right = std::iter::repeat(pad_char)
147
13
            .take(pad_width - ((pad_width / 2) + (pad_width % 2)))
148
13
            .collect::<String>();
149
13
        let positions: [&str; 3] = match align {
150
7
            Align::Left => [&pad_left, &pad_right, &cased_root],
151
3
            Align::Right => [&cased_root, &pad_left, &pad_right],
152
3
            Align::Center => [&pad_left, &cased_root, &pad_right],
153
        };
154
52
        for 
item39
in positions {
155
39
            result.push_str(item);
156
39
        }
157
    } else {
158
567k
        result.push_str(&cased_root);
159
567k
        let max_len = opt.width.unwrap_or(cased_root.len());
160
567k
        if cased_root.len() > max_len {
161
1.96k
            let max_idx = cased_root
162
1.96k
                .char_indices()
163
1.96k
                .map(|x| x.0)
164
2.08k
                .
rfind1.96k
(|x| *x <= max_len)
165
1.96k
                .unwrap_or(0);
166
1.96k
            result.truncate(max_idx);
167
565k
        }
168
    }
169
567k
    result
170
567k
}
171
172
367k
pub fn fmt_number<T: itoa::Integer + NumAssign + Signed + PartialOrd + ToPrimitive>(
173
367k
    n: T,
174
367k
    opt: DisplayOptions,
175
367k
) -> String {
176
367k
    let root = match 
opt.numerals0
{
177
        Some(Numerals::Roman) => {
178
0
            if n > T::zero() && n.to_i16().is_some() {
179
0
                format!("{:X}", Roman::from(n.to_i16().expect("Checked in if")))
180
            } else {
181
0
                "".to_string()
182
            }
183
        }
184
        _ => {
185
367k
            let mut root_buffer = itoa::Buffer::new();
186
367k
            root_buffer.format(n.abs()).to_string()
187
        }
188
    };
189
367k
    let prefix = match (opt.sign, n >= T::zero()) {
190
3
        (Sign::Always, true) => "+",
191
3
        (Sign::Always, false) => "-",
192
297k
        (Sign::OnlyNegative, true) => "",
193
263
        (Sign::OnlyNegative, false) => "-",
194
69.6k
        (Sign::Never, _) => "",
195
    };
196
367k
    let mut joined = String::from(prefix);
197
367k
    if opt.padding == Some('0') && 
opt.align221k
.unwrap_or(Align::Left) == Align::Left {
198
221k
        let non_pad_width = prefix.len() + root.len();
199
221k
        let arg_width = opt.width.unwrap_or(non_pad_width);
200
221k
        let pad_width = max(arg_width, non_pad_width) - non_pad_width;
201
221k
        let padding = std::iter::repeat('0').take(pad_width).collect::<String>();
202
221k
        joined.push_str(&padding);
203
221k
    
}145k
204
367k
    joined.push_str(&root);
205
367k
    fmt_string(&joined, opt)
206
367k
}
207
208
8.70k
pub fn fmt_days_since_epoch<T: Epoch + ToFixed>(t: T, opt: DisplayOptions) -> String {
209
8.70k
    fmt_number(t.to_fixed().get_day_i() - T::epoch().get_day_i(), opt)
210
8.70k
}
211
212
8.70k
pub fn fmt_seconds_since_epoch<T: Epoch + ToFixed>(t: T, opt: DisplayOptions) -> String {
213
8.70k
    fmt_number(
214
8.70k
        ((t.to_fixed().get() - T::epoch().get()) * (24.0 * 60.0 * 60.0)) as i64,
215
8.70k
        opt,
216
    )
217
8.70k
}
218
219
0
pub fn fmt_quarter<T: Quarter>(t: T, opt: DisplayOptions) -> String {
220
0
    fmt_number(t.quarter().get() as i16, opt)
221
0
}
222
223
impl DisplayItem for CommonDate {
224
0
    fn supported_lang(_lang: Language) -> bool {
225
0
        true
226
0
    }
227
228
253k
    fn fmt_numeric(&self, n: NumericContent, opt: DisplayOptions) -> String {
229
253k
        match n {
230
40.9k
            NumericContent::Month => fmt_number(self.month as i16, opt),
231
99.7k
            NumericContent::DayOfMonth => fmt_number(self.day as i16, opt),
232
113k
            NumericContent::Year => fmt_number(self.year, opt),
233
0
            _ => String::from(""),
234
        }
235
253k
    }
236
0
    fn fmt_text(&self, _t: TextContent, lang: Language, _opt: DisplayOptions) -> String {
237
0
        String::from("")
238
0
    }
239
}
240
241
impl DisplayItem for OrdinalDate {
242
0
    fn supported_lang(_lang: Language) -> bool {
243
0
        true
244
0
    }
245
246
8.19k
    fn fmt_numeric(&self, n: NumericContent, opt: DisplayOptions) -> String {
247
8.19k
        match n {
248
8.19k
            NumericContent::DayOfYear => fmt_number(self.day_of_year as i16, opt),
249
0
            NumericContent::Year => fmt_number(self.year, opt),
250
0
            _ => String::from(""),
251
        }
252
8.19k
    }
253
0
    fn fmt_text(&self, _t: TextContent, lang: Language, _opt: DisplayOptions) -> String {
254
0
        String::from("")
255
0
    }
256
}
257
258
265k
pub fn get_dict(lang: Language) -> &'static Dictionary<'static> {
259
265k
    match (lang) {
260
250k
        Language::EN => &EN_DICTIONARY,
261
15.1k
        Language::FR => &FR_DICTIONARY,
262
    }
263
265k
}
264
265
#[cfg(test)]
266
mod tests {
267
    use super::*;
268
269
    #[test]
270
1
    fn basic_number() {
271
1
        let opt_0 = DisplayOptions {
272
1
            numerals: None,
273
1
            width: None,
274
1
            align: None,
275
1
            padding: None,
276
1
            case: None,
277
1
            sign: Sign::Never,
278
1
        };
279
1
        assert_eq!(fmt_number(2025, opt_0), "2025");
280
1
        assert_eq!(fmt_number(-2025, opt_0), "2025");
281
1
        let opt_1 = DisplayOptions {
282
1
            numerals: None,
283
1
            width: None,
284
1
            align: None,
285
1
            padding: None,
286
1
            case: None,
287
1
            sign: Sign::Always,
288
1
        };
289
1
        assert_eq!(fmt_number(2025, opt_1), "+2025");
290
1
        assert_eq!(fmt_number(-2025, opt_1), "-2025");
291
1
        let opt_2 = DisplayOptions {
292
1
            numerals: None,
293
1
            width: None,
294
1
            align: None,
295
1
            padding: None,
296
1
            case: None,
297
1
            sign: Sign::OnlyNegative,
298
1
        };
299
1
        assert_eq!(fmt_number(2025, opt_2), "2025");
300
1
        assert_eq!(fmt_number(-2025, opt_2), "-2025");
301
1
    }
302
303
    #[test]
304
1
    fn basic_text() {
305
1
        let opt_0 = DisplayOptions {
306
1
            numerals: None,
307
1
            width: None,
308
1
            align: None,
309
1
            padding: None,
310
1
            case: None,
311
1
            sign: Sign::Never,
312
1
        };
313
1
        assert_eq!(fmt_string("January", opt_0), "January");
314
1
    }
315
316
    #[test]
317
1
    fn case_text() {
318
1
        let opt_0 = DisplayOptions {
319
1
            numerals: None,
320
1
            width: None,
321
1
            align: None,
322
1
            padding: None,
323
1
            case: Some(Case::Upper),
324
1
            sign: Sign::Never,
325
1
        };
326
1
        assert_eq!(fmt_string("mAy", opt_0), "MAY");
327
1
        let opt_1 = DisplayOptions {
328
1
            numerals: None,
329
1
            width: None,
330
1
            align: None,
331
1
            padding: None,
332
1
            case: Some(Case::Lower),
333
1
            sign: Sign::Never,
334
1
        };
335
1
        assert_eq!(fmt_string("mAy", opt_1), "may");
336
1
        let opt_2 = DisplayOptions {
337
1
            numerals: None,
338
1
            width: None,
339
1
            align: None,
340
1
            padding: None,
341
1
            case: Some(Case::Title),
342
1
            sign: Sign::Never,
343
1
        };
344
1
        assert_eq!(fmt_string("mAy", opt_2), "MAy");
345
1
        let opt_3 = DisplayOptions {
346
1
            numerals: None,
347
1
            width: None,
348
1
            align: None,
349
1
            padding: None,
350
1
            case: None,
351
1
            sign: Sign::Never,
352
1
        };
353
1
        assert_eq!(fmt_string("mAy", opt_3), "mAy");
354
1
    }
355
356
    #[test]
357
1
    fn pad_number() {
358
1
        let opt_0 = DisplayOptions {
359
1
            numerals: None,
360
1
            width: Some(8),
361
1
            align: None,
362
1
            padding: Some('@'),
363
1
            case: None,
364
1
            sign: Sign::Never,
365
1
        };
366
1
        assert_eq!(fmt_number(2025, opt_0), "@@@@2025");
367
1
        assert_eq!(fmt_number(-2025, opt_0), "@@@@2025");
368
1
        let opt_1 = DisplayOptions {
369
1
            numerals: None,
370
1
            width: Some(8),
371
1
            align: None,
372
1
            padding: Some('@'),
373
1
            case: None,
374
1
            sign: Sign::Always,
375
1
        };
376
1
        assert_eq!(fmt_number(2025, opt_1), "@@@+2025");
377
1
        assert_eq!(fmt_number(-2025, opt_1), "@@@-2025");
378
1
        let opt_2 = DisplayOptions {
379
1
            numerals: None,
380
1
            width: Some(8),
381
1
            align: None,
382
1
            padding: Some('0'),
383
1
            case: None,
384
1
            sign: Sign::Always,
385
1
        };
386
1
        assert_eq!(fmt_number(2025, opt_2), "+0002025");
387
1
        assert_eq!(fmt_number(-2025, opt_2), "-0002025");
388
1
    }
389
390
    #[test]
391
1
    fn align_number() {
392
1
        let opt_0 = DisplayOptions {
393
1
            numerals: None,
394
1
            width: Some(8),
395
1
            align: Some(Align::Left),
396
1
            padding: Some('@'),
397
1
            case: None,
398
1
            sign: Sign::OnlyNegative,
399
1
        };
400
1
        assert_eq!(fmt_number(2025, opt_0), "@@@@2025");
401
1
        assert_eq!(fmt_number(-2025, opt_0), "@@@-2025");
402
1
        let opt_1 = DisplayOptions {
403
1
            numerals: None,
404
1
            width: Some(8),
405
1
            align: Some(Align::Right),
406
1
            padding: Some('@'),
407
1
            case: None,
408
1
            sign: Sign::OnlyNegative,
409
1
        };
410
1
        assert_eq!(fmt_number(2025, opt_1), "2025@@@@");
411
1
        assert_eq!(fmt_number(-2025, opt_1), "-2025@@@");
412
1
        let opt_2 = DisplayOptions {
413
1
            numerals: None,
414
1
            width: Some(8),
415
1
            align: Some(Align::Center),
416
1
            padding: Some('@'),
417
1
            case: None,
418
1
            sign: Sign::OnlyNegative,
419
1
        };
420
1
        assert_eq!(fmt_number(2025, opt_2), "@@2025@@");
421
1
        assert_eq!(fmt_number(-2025, opt_2), "@@-2025@");
422
1
    }
423
424
    #[test]
425
1
    fn trunc_number() {
426
1
        let opt_0 = DisplayOptions {
427
1
            numerals: None,
428
1
            width: Some(2),
429
1
            align: None,
430
1
            padding: None,
431
1
            case: None,
432
1
            sign: Sign::OnlyNegative,
433
1
        };
434
1
        assert_eq!(fmt_number(2025, opt_0), "20");
435
1
        assert_eq!(fmt_number(-2025, opt_0), "-2");
436
1
    }
437
438
    #[test]
439
1
    fn align_text() {
440
1
        let opt_0 = DisplayOptions {
441
1
            numerals: None,
442
1
            width: Some(8),
443
1
            align: Some(Align::Left),
444
1
            padding: Some('@'),
445
1
            case: None,
446
1
            sign: Sign::Never,
447
1
        };
448
1
        assert_eq!(fmt_string("June", opt_0), "@@@@June");
449
1
        let opt_1 = DisplayOptions {
450
1
            numerals: None,
451
1
            width: Some(8),
452
1
            align: Some(Align::Right),
453
1
            padding: Some('@'),
454
1
            case: None,
455
1
            sign: Sign::Never,
456
1
        };
457
1
        assert_eq!(fmt_string("June", opt_1), "June@@@@");
458
1
        let opt_2 = DisplayOptions {
459
1
            numerals: None,
460
1
            width: Some(8),
461
1
            align: Some(Align::Center),
462
1
            padding: Some('@'),
463
1
            case: None,
464
1
            sign: Sign::Never,
465
1
        };
466
1
        assert_eq!(fmt_string("June", opt_2), "@@June@@");
467
1
    }
468
469
    #[test]
470
1
    fn trunc_text() {
471
1
        let opt_0 = DisplayOptions {
472
1
            numerals: None,
473
1
            width: Some(3),
474
1
            align: None,
475
1
            padding: None,
476
1
            case: None,
477
1
            sign: Sign::Never,
478
1
        };
479
1
        assert_eq!(fmt_string("January", opt_0), "Jan");
480
1
    }
481
482
    #[test]
483
1
    fn trunc_text_unicode() {
484
1
        let opt_0 = DisplayOptions {
485
1
            numerals: None,
486
1
            width: Some(1),
487
1
            align: None,
488
1
            padding: None,
489
1
            case: None,
490
1
            sign: Sign::Never,
491
1
        };
492
1
        assert_eq!(fmt_string("😀", opt_0), "");
493
1
        assert_eq!(fmt_string("😀😂", opt_0), "");
494
1
        let opt_1 = DisplayOptions {
495
1
            numerals: None,
496
1
            width: Some(4),
497
1
            align: None,
498
1
            padding: None,
499
1
            case: None,
500
1
            sign: Sign::Never,
501
1
        };
502
1
        assert_eq!(fmt_string("😀", opt_1), "😀");
503
1
        assert_eq!(fmt_string("😀😂", opt_1), "😀");
504
1
    }
505
}