1use crate::convert::implicitly_convert_to_int;
9use crate::extn::core::symbol::Symbol;
10use crate::extn::prelude::*;
11
12const NANOS_IN_SECOND: i64 = 1_000_000_000;
13
14const MILLIS_IN_NANO: i64 = 1_000_000;
15const MICROS_IN_NANO: i64 = 1_000;
16const NANOS_IN_NANO: i64 = 1;
17
18#[expect(clippy::cast_precision_loss, reason = "this cast is intentionally lossy")]
19const MIN_FLOAT_SECONDS: f64 = i64::MIN as f64;
20#[expect(clippy::cast_precision_loss, reason = "this cast is intentionally lossy")]
21const MAX_FLOAT_SECONDS: f64 = i64::MAX as f64;
22const MIN_FLOAT_NANOS: f64 = 0.0;
23#[expect(
24 clippy::cast_precision_loss,
25 reason = "NANOS_IN_SECOND is always in range of f64 without loss"
26)]
27const MAX_FLOAT_NANOS: f64 = NANOS_IN_SECOND as f64;
28#[expect(
29 clippy::cast_precision_loss,
30 reason = "NANOS_IN_SECOND is always in range of f64 without loss"
31)]
32const NANOS_IN_SECOND_F64: f64 = NANOS_IN_SECOND as f64;
33
34enum SubsecMultiplier {
35 Millis,
36 Micros,
37 Nanos,
38}
39
40impl SubsecMultiplier {
41 #[must_use]
42 const fn as_nanos(&self) -> i64 {
43 match self {
44 Self::Millis => MILLIS_IN_NANO,
45 Self::Micros => MICROS_IN_NANO,
46 Self::Nanos => NANOS_IN_NANO,
47 }
48 }
49}
50
51impl TryConvertMut<Option<Value>, SubsecMultiplier> for Artichoke {
52 type Error = Error;
53
54 fn try_convert_mut(&mut self, subsec_type: Option<Value>) -> Result<SubsecMultiplier, Self::Error> {
55 let mut subsec_type = match subsec_type {
56 Some(t) => t,
57 None => return Ok(SubsecMultiplier::Micros),
58 };
59
60 let subsec_type_symbol = if let Ruby::Symbol = subsec_type.ruby_type() {
61 unsafe { Symbol::unbox_from_value(&mut subsec_type, self)? }.bytes(self)
62 } else {
63 let mut message = b"unexpected unit: ".to_vec();
64 message.extend_from_slice(subsec_type.inspect(self).as_slice());
65 return Err(ArgumentError::from(message).into());
66 };
67
68 match subsec_type_symbol {
69 b"milliseconds" => Ok(SubsecMultiplier::Millis),
70 b"usec" => Ok(SubsecMultiplier::Micros),
71 b"nsec" => Ok(SubsecMultiplier::Nanos),
72 _ => {
73 let mut message = b"unexpected unit: ".to_vec();
74 message.extend_from_slice(subsec_type_symbol);
75 Err(ArgumentError::from(message).into())
76 }
77 }
78 }
79}
80
81#[derive(Debug, Copy, Clone)]
91pub struct Subsec {
92 secs: i64,
93 nanos: u32,
94}
95
96impl Subsec {
97 #[must_use]
101 pub fn to_tuple(self) -> (i64, u32) {
102 (self.secs, self.nanos)
103 }
104}
105
106impl TryConvertMut<(Option<Value>, Option<Value>), Subsec> for Artichoke {
107 type Error = Error;
108
109 fn try_convert_mut(&mut self, params: (Option<Value>, Option<Value>)) -> Result<Subsec, Self::Error> {
110 let (subsec, subsec_unit) = params;
111
112 let subsec = match subsec {
113 Some(subsec) => subsec,
114 None => return Ok(Subsec { secs: 0, nanos: 0 }),
115 };
116
117 let multiplier: SubsecMultiplier = self.try_convert_mut(subsec_unit)?;
118 let multiplier_nanos = multiplier.as_nanos();
119 let seconds_base = NANOS_IN_SECOND / multiplier_nanos;
124
125 if subsec.ruby_type() == Ruby::Float {
126 let subsec: f64 = self.try_convert(subsec)?;
131
132 if subsec.is_nan() {
133 return Err(FloatDomainError::with_message("NaN").into());
134 }
135 if subsec.is_infinite() {
136 if subsec.is_sign_negative() {
137 return Err(FloatDomainError::with_message("-Infinity").into());
138 }
139 return Err(FloatDomainError::with_message("Infinity").into());
140 }
141
142 #[expect(
146 clippy::cast_precision_loss,
147 reason = "guaranteed to be represented without loss in a f64"
148 )]
149 let seconds_base = seconds_base as f64;
150 #[expect(
151 clippy::cast_precision_loss,
152 reason = "guaranteed to be represented without loss in a f64"
153 )]
154 let multiplier_nanos = multiplier_nanos as f64;
155
156 let mut secs = subsec / seconds_base;
157 let mut nanos = (subsec % seconds_base) * multiplier_nanos;
158
159 if subsec < -0.0 {
162 secs -= 1.0;
167 if nanos != 0.0 && nanos != -0.0 {
168 nanos += NANOS_IN_SECOND_F64;
169 }
170 }
171
172 if !(MIN_FLOAT_SECONDS..=MAX_FLOAT_SECONDS).contains(&secs)
173 || !(MIN_FLOAT_NANOS..=MAX_FLOAT_NANOS).contains(&nanos)
174 {
175 return Err(ArgumentError::with_message("subsec outside of bounds").into());
176 }
177
178 #[expect(
179 clippy::cast_possible_truncation,
180 clippy::cast_sign_loss,
181 reason = "nanos and secs will always be in range due to bounds check."
182 )]
183 Ok(Subsec {
184 secs: secs as i64,
185 nanos: nanos as u32,
186 })
187 } else {
188 let subsec: i64 = implicitly_convert_to_int(self, subsec)?;
189
190 let mut secs = subsec / seconds_base;
194 let mut nanos = (subsec % seconds_base) * multiplier_nanos;
195
196 if subsec.is_negative() {
197 secs = secs
202 .checked_sub(1)
203 .ok_or(ArgumentError::with_message("Time too small"))?;
204
205 if nanos.signum() != 0 {
206 nanos += NANOS_IN_SECOND;
207 }
208 }
209
210 #[expect(
211 clippy::cast_possible_truncation,
212 clippy::cast_sign_loss,
213 reason = "nanos will always be less than `NANOS_IN_SECOND` which is in u32 range due to modulo and negative adjustments."
214 )]
215 Ok(Subsec {
216 secs,
217 nanos: nanos as u32,
218 })
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use bstr::ByteSlice;
226
227 use super::Subsec;
228 use crate::test::prelude::*;
229
230 fn subsec(interp: &mut Artichoke, params: (Option<&[u8]>, Option<&[u8]>)) -> Result<Subsec, Error> {
231 let (subsec, subsec_type) = params;
232 let subsec = subsec.map(|s| interp.eval(s).unwrap());
233 let subsec_type = subsec_type.map(|s| interp.eval(s).unwrap());
234
235 interp.try_convert_mut((subsec, subsec_type))
236 }
237
238 #[test]
239 fn no_subsec_provided() {
240 let mut interp = interpreter();
241
242 let result: Subsec = interp.try_convert_mut((None, None)).unwrap();
243 let (secs, nanos) = result.to_tuple();
244 assert_eq!(secs, 0);
245 assert_eq!(nanos, 0);
246 }
247
248 #[test]
249 fn no_subsec_provided_but_has_unit() {
250 let mut interp = interpreter();
251 let unit = interp.eval(b":usec").unwrap();
252
253 let result: Subsec = interp.try_convert_mut((None, Some(unit))).unwrap();
254 let (secs, nanos) = result.to_tuple();
255 assert_eq!(secs, 0);
256 assert_eq!(nanos, 0);
257 }
258
259 #[test]
260 fn int_no_unit_implies_micros() {
261 let mut interp = interpreter();
262
263 let expectations = [
264 (b"-1000001".as_slice(), (-2, 999_999_000)),
265 (b"-1000000".as_slice(), (-2, 0)),
266 (b"-999999".as_slice(), (-1, 1_000)),
267 (b"-1".as_slice(), (-1, 999_999_000)),
268 (b"0".as_slice(), (0, 0)),
269 (b"1".as_slice(), (0, 1_000)),
270 (b"999999".as_slice(), (0, 999_999_000)),
271 (b"1000000".as_slice(), (1, 0)),
272 (b"1000001".as_slice(), (1, 1_000)),
273 ];
274
275 let subsec_unit: Option<&[u8]> = None;
276
277 for (input, expectation) in &expectations {
278 let result = subsec(&mut interp, (Some(input), subsec_unit)).unwrap();
279 assert_eq!(
280 result.to_tuple(),
281 *expectation,
282 "Expected TryConvertMut<(Some({}), None), Result<Subsec>>, to return {} secs, {} nanos",
283 input.as_bstr(),
284 expectation.0,
285 expectation.1
286 );
287 }
288 }
289
290 #[test]
291 fn int_subsec_millis() {
292 let mut interp = interpreter();
293
294 let expectations = [
295 (b"-1001".as_slice(), (-2, 999_000_000)),
296 (b"-1000".as_slice(), (-2, 0)),
297 (b"-999".as_slice(), (-1, 1_000_000)),
298 (b"-1".as_slice(), (-1, 999_000_000)),
299 (b"0".as_slice(), (0, 0)),
300 (b"1".as_slice(), (0, 1_000_000)),
301 (b"999".as_slice(), (0, 999_000_000)),
302 (b"1000".as_slice(), (1, 0)),
303 (b"1001".as_slice(), (1, 1_000_000)),
304 ];
305
306 let subsec_unit: &[u8] = b":milliseconds";
307
308 for (input, expectation) in &expectations {
309 let result = subsec(&mut interp, (Some(input), Some(subsec_unit))).unwrap();
310 assert_eq!(
311 result.to_tuple(),
312 *expectation,
313 "Expected TryConvertMut<(Some({}), Some({})), Result<Subsec>>, to return {} secs, {} nanos",
314 input.as_bstr(),
315 subsec_unit.as_bstr(),
316 expectation.0,
317 expectation.1
318 );
319 }
320 }
321
322 #[test]
323 fn int_subsec_micros() {
324 let mut interp = interpreter();
325
326 let expectations = [
327 (b"-1000001".as_slice(), (-2, 999_999_000)),
328 (b"-1000000".as_slice(), (-2, 0)),
329 (b"-999999".as_slice(), (-1, 1_000)),
330 (b"-1".as_slice(), (-1, 999_999_000)),
331 (b"0".as_slice(), (0, 0)),
332 (b"1".as_slice(), (0, 1_000)),
333 (b"999999".as_slice(), (0, 999_999_000)),
334 (b"1000000".as_slice(), (1, 0)),
335 (b"1000001".as_slice(), (1, 1_000)),
336 ];
337
338 let subsec_unit: &[u8] = b":usec";
339
340 for (input, expectation) in &expectations {
341 let result = subsec(&mut interp, (Some(input), Some(subsec_unit))).unwrap();
342 assert_eq!(
343 result.to_tuple(),
344 *expectation,
345 "Expected TryConvertMut<(Some({}), Some({})), Result<Subsec>>, to return {} secs, {} nanos",
346 input.as_bstr(),
347 subsec_unit.as_bstr(),
348 expectation.0,
349 expectation.1
350 );
351 }
352 }
353
354 #[test]
355 fn int_subsec_nanos() {
356 let mut interp = interpreter();
357
358 let expectations = [
359 (b"-1000000001".as_slice(), (-2, 999_999_999)),
360 (b"-1000000000".as_slice(), (-2, 0)),
361 (b"-999999999".as_slice(), (-1, 1)),
362 (b"-1".as_slice(), (-1, 999_999_999)),
363 (b"0".as_slice(), (0, 0)),
364 (b"1".as_slice(), (0, 1)),
365 (b"999999999".as_slice(), (0, 999_999_999)),
366 (b"1000000000".as_slice(), (1, 0)),
367 (b"1000000001".as_slice(), (1, 1)),
368 ];
369
370 let subsec_unit: &[u8] = b":nsec";
371
372 for (input, expectation) in &expectations {
373 let result = subsec(&mut interp, (Some(input), Some(subsec_unit))).unwrap();
374 assert_eq!(
375 result.to_tuple(),
376 *expectation,
377 "Expected TryConvertMut<(Some({}), Some({})), Result<Subsec>>, to return {} secs, {} nanos",
378 input.as_bstr(),
379 subsec_unit.as_bstr(),
380 expectation.0,
381 expectation.1
382 );
383 }
384 }
385
386 #[test]
387 fn float_no_unit_implies_micros() {
388 let mut interp = interpreter();
389
390 let expectations = [
391 (b"-1000000.5".as_slice(), (-2, 999_999_500)),
393 (b"-1000000.0".as_slice(), (-2, 0)),
394 (b"-999999.5".as_slice(), (-1, 500)),
395 (b"-999999.0".as_slice(), (-1, 1_000)),
396 (b"-1000.5".as_slice(), (-1, 998_999_500)),
397 (b"-1.5".as_slice(), (-1, 999_998_500)),
398 (b"-1.0".as_slice(), (-1, 999_999_000)),
399 (b"-0.0".as_slice(), (0, 0)),
400 (b"0.0".as_slice(), (0, 0)),
401 (b"1.0".as_slice(), (0, 1_000)),
402 (b"1.5".as_slice(), (0, 1_500)),
403 (b"1000.5".as_slice(), (0, 1_000_500)),
404 (b"999999.0".as_slice(), (0, 999_999_000)),
405 (b"999999.5".as_slice(), (0, 999_999_500)),
406 (b"1000000.0".as_slice(), (1, 0)),
407 (b"1000000.5".as_slice(), (1, 500)),
408 (b"1000001.0".as_slice(), (1, 1000)),
409 (b"0.123".as_slice(), (0, 123)),
411 (b"0.001".as_slice(), (0, 1)),
412 (b"0.0001".as_slice(), (0, 0)),
413 (b"0.0009".as_slice(), (0, 0)),
414 ];
415
416 let subsec_unit: Option<&[u8]> = None;
417
418 for (input, expectation) in &expectations {
419 let result = subsec(&mut interp, (Some(input), subsec_unit)).unwrap();
420 assert_eq!(
421 result.to_tuple(),
422 *expectation,
423 "Expected TryConvertMut<(Some({}), None), Result<Subsec>>, to return {} secs, {} nanos",
424 input.as_bstr(),
425 expectation.0,
426 expectation.1
427 );
428 }
429 }
430
431 #[test]
432 fn float_subsec_millis() {
433 let mut interp = interpreter();
434
435 let expectations = [
436 (b"-1000.5".as_slice(), (-2, 999_500_000)),
438 (b"-1000.0".as_slice(), (-2, 0)),
439 (b"-999.5".as_slice(), (-1, 500_000)),
440 (b"-999.0".as_slice(), (-1, 1_000_000)),
441 (b"-1.5".as_slice(), (-1, 998_500_000)),
442 (b"-1.0".as_slice(), (-1, 999_000_000)),
443 (b"-0.0".as_slice(), (0, 0)),
444 (b"0.0".as_slice(), (0, 0)),
445 (b"1.0".as_slice(), (0, 1_000_000)),
446 (b"1.5".as_slice(), (0, 1_500_000)),
447 (b"999.0".as_slice(), (0, 999_000_000)),
448 (b"999.5".as_slice(), (0, 999_500_000)),
449 (b"1000.0".as_slice(), (1, 0)),
450 (b"1000.5".as_slice(), (1, 500_000)),
451 (b"1001.0".as_slice(), (1, 1_000_000)),
452 (b"0.123456".as_slice(), (0, 123_456)),
454 (b"0.000001".as_slice(), (0, 1)),
455 (b"0.0000001".as_slice(), (0, 0)),
456 (b"0.0000009".as_slice(), (0, 0)),
457 ];
458
459 let subsec_unit: Option<&[u8]> = Some(b":milliseconds");
460
461 for (input, expectation) in &expectations {
462 let result = subsec(&mut interp, (Some(input), subsec_unit)).unwrap();
463 assert_eq!(
464 result.to_tuple(),
465 *expectation,
466 "Expected TryConvertMut<(Some({}), None), Result<Subsec>>, to return {} secs, {} nanos",
467 input.as_bstr(),
468 expectation.0,
469 expectation.1
470 );
471 }
472 }
473
474 #[test]
475 fn float_subsec_micros() {
476 let mut interp = interpreter();
477
478 let expectations = [
479 (b"-1000000.5".as_slice(), (-2, 999_999_500)),
481 (b"-1000000.0".as_slice(), (-2, 0)),
482 (b"-999999.5".as_slice(), (-1, 500)),
483 (b"-999999.0".as_slice(), (-1, 1_000)),
484 (b"-1000.5".as_slice(), (-1, 998_999_500)),
485 (b"-1.5".as_slice(), (-1, 999_998_500)),
486 (b"-1.0".as_slice(), (-1, 999_999_000)),
487 (b"-0.0".as_slice(), (0, 0)),
488 (b"0.0".as_slice(), (0, 0)),
489 (b"1.0".as_slice(), (0, 1_000)),
490 (b"1.5".as_slice(), (0, 1_500)),
491 (b"1000.5".as_slice(), (0, 1_000_500)),
492 (b"999999.0".as_slice(), (0, 999_999_000)),
493 (b"999999.5".as_slice(), (0, 999_999_500)),
494 (b"1000000.0".as_slice(), (1, 0)),
495 (b"1000000.5".as_slice(), (1, 500)),
496 (b"1000001.0".as_slice(), (1, 1000)),
497 (b"0.123".as_slice(), (0, 123)),
499 (b"0.001".as_slice(), (0, 1)),
500 (b"0.0001".as_slice(), (0, 0)),
501 (b"0.0009".as_slice(), (0, 0)),
502 ];
503
504 let subsec_unit: Option<&[u8]> = Some(b":usec");
505
506 for (input, expectation) in &expectations {
507 let result = subsec(&mut interp, (Some(input), subsec_unit)).unwrap();
508 assert_eq!(
509 result.to_tuple(),
510 *expectation,
511 "Expected TryConvertMut<(Some({}), None), Result<Subsec>>, to return {} secs, {} nanos",
512 input.as_bstr(),
513 expectation.0,
514 expectation.1
515 );
516 }
517 }
518
519 #[test]
520 fn float_subsec_nanos() {
521 let mut interp = interpreter();
522
523 let expectations = [
524 (b"-1000000000.5".as_slice(), (-2, 999_999_999)),
526 (b"-1000000000.0".as_slice(), (-2, 0)),
527 (b"-999999999.5".as_slice(), (-1, 0)),
528 (b"-999999999.0".as_slice(), (-1, 1)),
529 (b"-1000.5".as_slice(), (-1, 999_998_999)),
530 (b"-1.5".as_slice(), (-1, 999_999_998)),
531 (b"-1.0".as_slice(), (-1, 999_999_999)),
532 (b"-0.0".as_slice(), (0, 0)),
533 (b"0.0".as_slice(), (0, 0)),
534 (b"1.0".as_slice(), (0, 1)),
535 (b"1.5".as_slice(), (0, 1)),
536 (b"1000.5".as_slice(), (0, 1_000)),
537 (b"999999999.0".as_slice(), (0, 999_999_999)),
538 (b"999999999.5".as_slice(), (0, 999_999_999)),
539 (b"1000000000.0".as_slice(), (1, 0)),
540 (b"1000000000.5".as_slice(), (1, 0)),
541 (b"1000000001.0".as_slice(), (1, 1)),
542 (b"-0.1".as_slice(), (-1, 999_999_999)),
544 (b"0.1".as_slice(), (0, 0)),
545 ];
546
547 let subsec_unit: Option<&[u8]> = Some(b":nsec");
548
549 for (input, expectation) in &expectations {
550 let result = subsec(&mut interp, (Some(input), subsec_unit)).unwrap();
551 assert_eq!(
552 result.to_tuple(),
553 *expectation,
554 "Expected TryConvertMut<(Some({}), None), Result<Subsec>>, to return {} secs, {} nanos",
555 input.as_bstr(),
556 expectation.0,
557 expectation.1
558 );
559 }
560 }
561
562 #[test]
563 fn float_nan_raises() {
564 let mut interp = interpreter();
565
566 let err = subsec(&mut interp, (Some(b"Float::NAN"), None)).unwrap_err();
567
568 assert_eq!(err.name(), "FloatDomainError");
569 assert_eq!(err.message(), b"NaN".as_slice());
570 }
571
572 #[test]
573 fn float_infinite_raises() {
574 let mut interp = interpreter();
575
576 let err = subsec(&mut interp, (Some(b"Float::INFINITY"), None)).unwrap_err();
577
578 assert_eq!(err.name(), "FloatDomainError");
579 assert_eq!(err.message().as_bstr(), b"Infinity".as_bstr());
580
581 let err = subsec(&mut interp, (Some(b"-Float::INFINITY"), None)).unwrap_err();
582
583 assert_eq!(err.name(), "FloatDomainError");
584 assert_eq!(err.message().as_bstr(), b"-Infinity".as_bstr());
585 }
586
587 #[test]
588 fn invalid_subsec_unit() {
589 let mut interp = interpreter();
590
591 let err = subsec(&mut interp, (Some(b"1"), Some(b":bad_unit"))).unwrap_err();
592
593 assert_eq!(err.name(), "ArgumentError");
594 assert_eq!(err.message().as_bstr(), b"unexpected unit: bad_unit".as_bstr());
595 }
596
597 #[test]
598 fn subsec_unit_non_symbol() {
599 let mut interp = interpreter();
600
601 let err = subsec(&mut interp, (Some(b"1"), Some(b":bad_unit"))).unwrap_err();
602
603 assert_eq!(err.name(), "ArgumentError");
604 assert_eq!(err.message().as_bstr(), b"unexpected unit: bad_unit".as_bstr());
605
606 let err = subsec(&mut interp, (Some(b"1"), Some(b"1"))).unwrap_err();
607
608 assert_eq!(err.name(), "ArgumentError");
609 assert_eq!(err.message().as_bstr(), b"unexpected unit: 1".as_bstr());
610
611 let err = subsec(&mut interp, (Some(b"1"), Some(b"Object.new"))).unwrap_err();
612
613 assert_eq!(err.name(), "ArgumentError");
614 assert!(
615 err.message()
616 .as_bstr()
617 .starts_with(b"unexpected unit: #<Object:".as_bstr())
618 );
619 }
620
621 #[test]
622 fn subsec_unit_requires_explicit_symbol() {
623 let mut interp = interpreter();
624
625 let err = subsec(
626 &mut interp,
627 (Some(b"1"), Some(b"class A; def to_sym; :usec; end; end && A.new")),
628 )
629 .unwrap_err();
630
631 assert_eq!(err.name(), "ArgumentError");
632 assert!(err.message().as_bstr().starts_with(b"unexpected unit: #<A:".as_bstr()));
633 }
634}