artichoke_backend/extn/core/time/
args.rs

1use crate::convert::{to_int, to_str};
2use crate::extn::core::kernel::integer;
3use crate::extn::prelude::*;
4
5#[derive(Debug, Copy, Clone)]
6pub struct Args {
7    pub year: i32,
8    pub month: u8,
9    pub day: u8,
10    pub hour: u8,
11    pub minute: u8,
12    pub second: u8,
13    pub nanoseconds: u32,
14}
15
16impl Default for Args {
17    fn default() -> Self {
18        Self {
19            year: 0,
20            month: 1,
21            day: 1,
22            hour: 0,
23            minute: 0,
24            second: 0,
25            nanoseconds: 0,
26        }
27    }
28}
29
30impl TryConvertMut<&mut [Value], Args> for Artichoke {
31    type Error = Error;
32
33    fn try_convert_mut(&mut self, mut args: &mut [Value]) -> Result<Args, Self::Error> {
34        // Time args should have a length of 1..=8 or 10. The error does not
35        // give a hint that the 10 arg variant is supported however (this is
36        // the same in MRI).
37        if let 0 | 9 | 11 = args.len() {
38            let mut message = b"wrong number of arguments (given ".to_vec();
39            message.extend(args.len().to_string().bytes());
40            message.extend_from_slice(b", expected 1..8)");
41            return Err(ArgumentError::from(message).into());
42        }
43
44        // Args are in order of year, month, day, hour, minute, second, micros.
45        // This is unless there are 10 arguments provided (`Time#to_a` format),
46        // at which points it is second, minute, hour, day, month, year.
47        if args.len() == 10 {
48            args.swap(0, 5);
49            args.swap(1, 4);
50            args.swap(2, 3);
51            // All arguments after position 5 are ignored in the 10 argument
52            // variant.
53            args = &mut args[..6];
54        }
55
56        let mut result = Args::default();
57
58        for (i, &arg) in args.iter().enumerate() {
59            match i {
60                0 => {
61                    let arg: i64 = to_int(self, arg).and_then(|arg| arg.try_convert_into(self))?;
62
63                    result.year = i32::try_from(arg).map_err(|_| ArgumentError::with_message("year out of range"))?;
64                }
65                // Short circuit month checking to avoid `to_str` checking.
66                1 if Ruby::Fixnum == arg.ruby_type() => {
67                    let arg = to_int(self, arg)?;
68                    let arg: i64 = arg.try_convert_into(self)?;
69
70                    result.month = match u8::try_from(arg) {
71                        Ok(month @ 1..=12) => month,
72                        _ => return Err(ArgumentError::with_message("mon out of range").into()),
73                    };
74                }
75                1 => {
76                    // ```irb
77                    // 3.1.2 => Time.utc(2022, 2).month
78                    // => 2
79                    // 3.1.2 => class I; def to_int; 2; end; end
80                    // => :to_int
81                    // 3.1.2 => Time.utc(2022, I.new).month
82                    // => 2
83                    // 3.1.2 > Time.utc(2022, "feb").month
84                    // => 2
85                    // 3.1.2 > class A; def to_str; "feb"; end; end
86                    // => :to_str
87                    // 3.1.2 > Time.utc(2022, A.new).month
88                    // => 2
89                    // 3.1.2 > class I; def to_str; "2"; end; end
90                    // => :to_str
91                    // 3.1.2 > Time.utc(2022, I.new).month
92                    // => 2
93                    // ```
94                    let month: i64 = if let Ok(arg) = to_str(self, arg) {
95                        let mut month_str: Vec<u8> = arg.try_convert_into_mut(self)?;
96
97                        // Valid month string args are always 3 bytes long (or 3
98                        // ASCII characters). only downcase the first 3 bytes to
99                        // avoid excessive resource consumption if given a long
100                        // string.
101                        let month_str = month_str.get_mut(..3).unwrap_or_default();
102                        month_str.make_ascii_lowercase();
103
104                        match &*month_str {
105                            b"jan" => 1,
106                            b"feb" => 2,
107                            b"mar" => 3,
108                            b"apr" => 4,
109                            b"may" => 5,
110                            b"jun" => 6,
111                            b"jul" => 7,
112                            b"aug" => 8,
113                            b"sep" => 9,
114                            b"oct" => 10,
115                            b"nov" => 11,
116                            b"dec" => 12,
117                            _ => {
118                                // Delegate to `Kernel#Integer` as last resort
119                                // to handle Integer strings.
120                                let arg = integer(self, arg, None)?;
121                                arg.try_convert_into(self)?
122                            }
123                        }
124                    } else {
125                        let arg = to_int(self, arg)?;
126                        arg.try_convert_into(self)?
127                    };
128
129                    result.month = match u8::try_from(month) {
130                        Ok(month @ 1..=12) => month,
131                        _ => return Err(ArgumentError::with_message("mon out of range").into()),
132                    };
133                }
134                2 => {
135                    let arg = to_int(self, arg)?;
136                    let arg: i64 = arg.try_convert_into(self)?;
137
138                    result.day = match u8::try_from(arg) {
139                        Ok(day @ 1..=31) => day,
140                        _ => return Err(ArgumentError::with_message("mday out of range").into()),
141                    };
142                }
143                3 => {
144                    let arg = to_int(self, arg)?;
145                    let arg: i64 = arg.try_convert_into(self)?;
146
147                    result.hour = match u8::try_from(arg) {
148                        Ok(hour @ 0..=59) => hour,
149                        _ => return Err(ArgumentError::with_message("hour out of range").into()),
150                    };
151                }
152                4 => {
153                    let arg = to_int(self, arg)?;
154                    let arg: i64 = arg.try_convert_into(self)?;
155
156                    result.minute = match u8::try_from(arg) {
157                        Ok(minute @ 0..=59) => minute,
158                        _ => return Err(ArgumentError::with_message("min out of range").into()),
159                    };
160                }
161                5 => {
162                    // TODO: This should support `f64` seconds and drop the
163                    // remainder into micros.
164                    // ```irb
165                    // 3.1.2 > Time.utc(1, 2, 3, 4, 5, 6.1)
166                    // => 0001-02-03 04:05:06 56294995342131/562949953421312 UTC
167                    // ```
168                    let arg = to_int(self, arg)?;
169                    let arg: i64 = arg.try_convert_into(self)?;
170
171                    result.second = match u8::try_from(arg) {
172                        Ok(second @ 0..=60) => second,
173                        _ => return Err(ArgumentError::with_message("sec out of range").into()),
174                    };
175                }
176                6 => {
177                    let arg = to_int(self, arg)?;
178                    let arg: i64 = arg.try_convert_into(self)?;
179
180                    // Args take a micros parameter, not a nanos value, and
181                    // therefore we must multiply the value by 1000. This is
182                    // guaranteed to fit in a `u32`.
183                    result.nanoseconds = match u32::try_from(arg) {
184                        Ok(micros @ 0..=999_999) => micros * 1000,
185                        // ```
186                        // [3.1.2] > Time.utc(2022, 12, 31, 13, 24, 55, 100000000000000000)
187                        // (irb):3:in `utc': subsecx out of range (ArgumentError)
188                        // ```
189                        _ => return Err(ArgumentError::with_message("subsecx out of range").into()),
190                    };
191                }
192                7 => {
193                    // NOOP: The 8th parameter can be anything, even an error
194                    //
195                    // ```irb
196                    // Time.utc(2022, 1, 1, 0, 0, 0, 0, StandardError)
197                    // => 2022-01-01 00:00:00 UTC
198                    // ```
199                }
200                _ => {
201                    // The 10 argument variant truncates, and the max length
202                    // other variants is 8, so this should always be
203                    // unreachable.
204                    unreachable!()
205                }
206            }
207        }
208
209        Ok(result)
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use bstr::ByteSlice;
216
217    use super::Args;
218    use crate::test::prelude::*;
219
220    #[test]
221    fn requires_at_least_one_param() {
222        let mut interp = interpreter();
223
224        let mut args = vec![];
225
226        let result: Result<Args, Error> = interp.try_convert_mut(args.as_mut_slice());
227        let error = result.unwrap_err();
228
229        assert_eq!(error.name(), "ArgumentError");
230        assert_eq!(
231            error.message().as_bstr(),
232            b"wrong number of arguments (given 0, expected 1..8)".as_bstr()
233        );
234    }
235
236    #[test]
237    fn eight_params() {
238        let mut interp = interpreter();
239
240        let args = interp.eval(b"[2022, 2, 3, 4, 5, 6, 7, nil]").unwrap();
241        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
242        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
243        assert_eq!(2022, result.year);
244        assert_eq!(2, result.month);
245        assert_eq!(3, result.day);
246        assert_eq!(4, result.hour);
247        assert_eq!(5, result.minute);
248        assert_eq!(6, result.second);
249        assert_eq!(7000, result.nanoseconds);
250    }
251
252    #[test]
253    fn seven_params() {
254        let mut interp = interpreter();
255
256        let args = interp.eval(b"[2022, 2, 3, 4, 5, 6, 7]").unwrap();
257        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
258        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
259        assert_eq!(2022, result.year);
260        assert_eq!(2, result.month);
261        assert_eq!(3, result.day);
262        assert_eq!(4, result.hour);
263        assert_eq!(5, result.minute);
264        assert_eq!(6, result.second);
265        assert_eq!(7000, result.nanoseconds);
266    }
267
268    #[test]
269    fn six_params() {
270        let mut interp = interpreter();
271
272        let args = interp.eval(b"[2022, 2, 3, 4, 5, 6]").unwrap();
273        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
274        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
275        assert_eq!(2022, result.year);
276        assert_eq!(2, result.month);
277        assert_eq!(3, result.day);
278        assert_eq!(4, result.hour);
279        assert_eq!(5, result.minute);
280        assert_eq!(6, result.second);
281        assert_eq!(0, result.nanoseconds);
282    }
283
284    #[test]
285    fn five_params() {
286        let mut interp = interpreter();
287
288        let args = interp.eval(b"[2022, 2, 3, 4, 5]").unwrap();
289        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
290        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
291        assert_eq!(2022, result.year);
292        assert_eq!(2, result.month);
293        assert_eq!(3, result.day);
294        assert_eq!(4, result.hour);
295        assert_eq!(5, result.minute);
296        assert_eq!(0, result.second);
297        assert_eq!(0, result.nanoseconds);
298    }
299
300    #[test]
301    fn four_params() {
302        let mut interp = interpreter();
303
304        let args = interp.eval(b"[2022, 2, 3, 4]").unwrap();
305        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
306        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
307        assert_eq!(2022, result.year);
308        assert_eq!(2, result.month);
309        assert_eq!(3, result.day);
310        assert_eq!(4, result.hour);
311        assert_eq!(0, result.minute);
312        assert_eq!(0, result.second);
313        assert_eq!(0, result.nanoseconds);
314    }
315
316    #[test]
317    fn three_params() {
318        let mut interp = interpreter();
319
320        let args = interp.eval(b"[2022, 2, 3]").unwrap();
321        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
322        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
323        assert_eq!(2022, result.year);
324        assert_eq!(2, result.month);
325        assert_eq!(3, result.day);
326        assert_eq!(0, result.hour);
327        assert_eq!(0, result.minute);
328        assert_eq!(0, result.second);
329        assert_eq!(0, result.nanoseconds);
330    }
331
332    #[test]
333    fn two_params() {
334        let mut interp = interpreter();
335
336        let args = interp.eval(b"[2022, 2]").unwrap();
337        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
338        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
339        assert_eq!(2022, result.year);
340        assert_eq!(2, result.month);
341        assert_eq!(1, result.day);
342        assert_eq!(0, result.hour);
343        assert_eq!(0, result.minute);
344        assert_eq!(0, result.second);
345        assert_eq!(0, result.nanoseconds);
346    }
347
348    #[test]
349    fn one_param() {
350        let mut interp = interpreter();
351
352        let args = interp.eval(b"[2022]").unwrap();
353        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
354        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
355        assert_eq!(2022, result.year);
356        assert_eq!(1, result.month);
357        assert_eq!(1, result.day);
358        assert_eq!(0, result.hour);
359        assert_eq!(0, result.minute);
360        assert_eq!(0, result.second);
361        assert_eq!(0, result.nanoseconds);
362    }
363
364    #[test]
365    fn month_supports_string_values() {
366        let mut interp = interpreter();
367
368        let table = [
369            (b"[2022, 'jan']", 1),
370            (b"[2022, 'feb']", 2),
371            (b"[2022, 'mar']", 3),
372            (b"[2022, 'apr']", 4),
373            (b"[2022, 'may']", 5),
374            (b"[2022, 'jun']", 6),
375            (b"[2022, 'jul']", 7),
376            (b"[2022, 'aug']", 8),
377            (b"[2022, 'sep']", 9),
378            (b"[2022, 'oct']", 10),
379            (b"[2022, 'nov']", 11),
380            (b"[2022, 'dec']", 12),
381        ];
382
383        for (input, expected_month) in table {
384            let args = interp.eval(input).unwrap();
385            let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
386            let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
387
388            assert_eq!(expected_month, result.month);
389        }
390    }
391
392    #[test]
393    fn month_strings_are_case_insensitive() {
394        let mut interp = interpreter();
395
396        let table = [
397            (b"[2022, 'Feb']", 2),
398            (b"[2022, 'fEb']", 2),
399            (b"[2022, 'feB']", 2),
400            (b"[2022, 'FEb']", 2),
401            (b"[2022, 'FeB']", 2),
402            (b"[2022, 'fEB']", 2),
403            (b"[2022, 'FEB']", 2),
404        ];
405
406        for (input, expected_month) in table {
407            let args = interp.eval(input).unwrap();
408            let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
409            let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
410
411            assert_eq!(expected_month, result.month);
412            assert_eq!(2022, result.year);
413        }
414    }
415
416    #[test]
417    fn month_supports_string_like_values() {
418        let mut interp = interpreter();
419
420        let args = interp
421            .eval(b"class A; def to_str; 'feb'; end; end; [2022, A.new]")
422            .unwrap();
423        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
424        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
425
426        assert_eq!(2, result.month);
427    }
428
429    #[test]
430    fn month_supports_int_like_values() {
431        let mut interp = interpreter();
432
433        let args = interp.eval(b"class A; def to_int; 2; end; end; [2022, A.new]").unwrap();
434        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
435        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
436
437        assert_eq!(2, result.month);
438    }
439
440    #[test]
441    fn month_string_can_be_integer_strings() {
442        let mut interp = interpreter();
443
444        let args = interp
445            .eval(b"class A; def to_str; '2'; end; end; [2022, A.new]")
446            .unwrap();
447        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
448        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
449
450        assert_eq!(2, result.month);
451    }
452
453    #[test]
454    fn invalid_month_string_responds_with_int_conversion_error() {
455        let mut interp = interpreter();
456
457        let args = interp
458            .eval(b"class A; def to_str; 'aaa'; end; end; [2022, A.new]")
459            .unwrap();
460        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
461        let result: Result<Args, Error> = interp.try_convert_mut(ary_args.as_mut_slice());
462        let error = result.unwrap_err();
463
464        assert_eq!(
465            error.message().as_bstr(),
466            br#"invalid value for Integer(): "aaa""#.as_bstr()
467        );
468        assert_eq!(error.name(), "ArgumentError");
469    }
470
471    #[test]
472    fn month_downcase_shortcut_does_not_limit_call_to_integer() {
473        let mut interp = interpreter();
474
475        let args = interp
476            .eval(b"class I; def to_str; '0000000002'; end; end; [2022, I.new]")
477            .unwrap();
478        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
479        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
480
481        assert_eq!(2, result.month);
482    }
483
484    #[test]
485    fn subsec_is_valid_micros_not_nanos() {
486        let mut interp = interpreter();
487
488        let args = interp.eval(b"[2022, 1, 1, 0, 0, 0, 1]").unwrap();
489        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
490        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
491        let nanos = result.nanoseconds;
492        assert_eq!(1000, nanos);
493
494        let args = interp.eval(b"[2022, 1, 1, 0, 0, 0, 999_999]").unwrap();
495        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
496        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
497        let nanos = result.nanoseconds;
498        assert_eq!(999_999_000, nanos);
499    }
500
501    #[test]
502    fn subsec_does_not_wrap_around() {
503        let mut interp = interpreter();
504
505        let args = interp.eval(b"[2022, 1, 1, 0, 0, 0, -1]").unwrap();
506        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
507        let result: Result<Args, Error> = interp.try_convert_mut(ary_args.as_mut_slice());
508        let error = result.unwrap_err();
509        assert_eq!(error.message().as_bstr(), b"subsecx out of range".as_bstr());
510
511        let args = interp.eval(b"[2022, 1, 1, 0, 0, 0, 1_000_000]").unwrap();
512        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
513        let result: Result<Args, Error> = interp.try_convert_mut(ary_args.as_mut_slice());
514        let error = result.unwrap_err();
515        assert_eq!(error.message().as_bstr(), b"subsecx out of range".as_bstr());
516    }
517
518    #[test]
519    #[should_panic(expected = "not yet implemented")]
520    fn fractional_seconds_return_nanos() {
521        // ```irb
522        // 3.1.2 > Time.utc(*[2022, 1, 1, 0, 0, 1.5])
523        // => 2022-01-01 00:00:01.5 UTC
524        // 3.1.2 > Time.utc(*[2022, 1, 1, 0, 0, 1.5]).subsec
525        // => (1/2)
526        // 3.1.2 > Time.utc(*[2022, 1, 1, 0, 0, 1.5]).tv_nsec
527        // => 500000000
528        // 3.1.2 > Time.utc(*[2022, 1, 1, 0, 0, 1.5, 0]).tv_nsec
529        // => 0
530        // ```
531        todo!("fractional seconds is not implemented")
532    }
533
534    #[test]
535    fn nine_args_not_supported() {
536        let mut interp = interpreter();
537
538        let args = interp.eval(b"[2022, 2, 3, 4, 5, 6, 7, nil, 0]").unwrap();
539        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
540        let result: Result<Args, Error> = interp.try_convert_mut(ary_args.as_mut_slice());
541        let error = result.unwrap_err();
542
543        assert_eq!(
544            error.message().as_bstr(),
545            b"wrong number of arguments (given 9, expected 1..8)".as_bstr()
546        );
547        assert_eq!(error.name(), "ArgumentError");
548    }
549
550    #[test]
551    fn ten_args_changes_unit_order() {
552        let mut interp = interpreter();
553
554        let args = interp.eval(b"[1, 2, 3, 4, 5, 2022, nil, nil, nil, nil]").unwrap();
555        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
556        let result: Args = interp.try_convert_mut(ary_args.as_mut_slice()).unwrap();
557
558        assert_eq!(1, result.second);
559        assert_eq!(2, result.minute);
560        assert_eq!(3, result.hour);
561        assert_eq!(4, result.day);
562        assert_eq!(5, result.month);
563        assert_eq!(2022, result.year);
564    }
565
566    #[test]
567    fn eleven_args_is_too_many() {
568        let mut interp = interpreter();
569
570        let args = interp.eval(b"[2022, 2, 3, 4, 5, 6, 7, nil, 0, 0, 0]").unwrap();
571        let mut ary_args: Vec<Value> = interp.try_convert_mut(args).unwrap();
572        let result: Result<Args, Error> = interp.try_convert_mut(ary_args.as_mut_slice());
573        let error = result.unwrap_err();
574
575        assert_eq!(
576            error.message().as_bstr(),
577            b"wrong number of arguments (given 11, expected 1..8)".as_bstr()
578        );
579        assert_eq!(error.name(), "ArgumentError");
580    }
581}