1use derive_more::{Display, Error};
9use jiff::civil::Date;
10use jiff::Timestamp;
11
12use crate::interpreter::value::*;
13use crate::interpreter::BuiltinContext;
14use minir_macros::{builtin, interpreter_builtin};
15
16#[derive(Debug, Display, Error)]
19pub enum DateTimeError {
20 #[display("character string is not in a standard unambiguous format")]
21 AmbiguousFormat,
22
23 #[display("invalid 'origin' argument")]
24 InvalidOrigin,
25
26 #[display("invalid date/time format: {}", reason)]
27 InvalidFormat { reason: String },
28}
29
30impl From<DateTimeError> for RError {
31 fn from(e: DateTimeError) -> Self {
32 RError::from_source(RErrorKind::Argument, e)
33 }
34}
35
36const EPOCH: Date = Date::constant(1970, 1, 1);
42
43fn days_to_date(days: f64) -> Option<Date> {
45 let days_i32 = days.round() as i32; EPOCH
47 .checked_add(jiff::Span::new().days(i64::from(days_i32)))
48 .ok()
49}
50
51fn date_to_days(date: Date) -> f64 {
53 let span = EPOCH.until(date).expect("valid date span");
55 f64::from(span.get_days())
56}
57
58fn r_date(days_since_epoch: f64) -> RValue {
60 let mut rv = RVector::from(Vector::Double(vec![Some(days_since_epoch)].into()));
61 rv.set_attr(
62 "class".to_string(),
63 RValue::vec(Vector::Character(vec![Some("Date".to_string())].into())),
64 );
65 rv.into()
66}
67
68fn r_posixct(secs_since_epoch: f64, tz: Option<&str>) -> RValue {
70 let mut rv = RVector::from(Vector::Double(vec![Some(secs_since_epoch)].into()));
71 rv.set_attr(
72 "class".to_string(),
73 RValue::vec(Vector::Character(
74 vec![Some("POSIXct".to_string()), Some("POSIXt".to_string())].into(),
75 )),
76 );
77 if let Some(tz) = tz {
78 rv.set_attr(
79 "tzone".to_string(),
80 RValue::vec(Vector::Character(vec![Some(tz.to_string())].into())),
81 );
82 }
83 rv.into()
84}
85
86fn r_posixct_vec(secs: Vec<Option<f64>>, tz: Option<&str>) -> RValue {
88 let mut rv = RVector::from(Vector::Double(secs.into()));
89 rv.set_attr(
90 "class".to_string(),
91 RValue::vec(Vector::Character(
92 vec![Some("POSIXct".to_string()), Some("POSIXt".to_string())].into(),
93 )),
94 );
95 if let Some(tz) = tz {
96 rv.set_attr(
97 "tzone".to_string(),
98 RValue::vec(Vector::Character(vec![Some(tz.to_string())].into())),
99 );
100 }
101 rv.into()
102}
103
104fn translate_format(fmt: &str) -> String {
107 fmt.replace("%OS", "%S%.f")
108}
109
110fn timestamp_to_secs(ts: &Timestamp) -> f64 {
112 (ts.as_second() as f64) + f64::from(ts.subsec_nanosecond()) / 1e9
114}
115
116fn parse_date_string(s: &str) -> Result<Date, DateTimeError> {
118 if let Ok(d) = s.parse::<Date>() {
120 return Ok(d);
121 }
122 for fmt in &["%Y-%m-%d", "%Y/%m/%d", "%m/%d/%Y", "%d/%m/%Y", "%b %d, %Y"] {
124 if let Ok(d) = Date::strptime(fmt, s) {
125 return Ok(d);
126 }
127 }
128 Err(DateTimeError::AmbiguousFormat)
129}
130
131fn parse_datetime_string(s: &str, tz: Option<&str>) -> Result<f64, DateTimeError> {
133 if let Ok(z) = s.parse::<jiff::Zoned>() {
135 return Ok(timestamp_to_secs(&z.timestamp()));
136 }
137 if let Ok(ts) = s.parse::<Timestamp>() {
139 return Ok(timestamp_to_secs(&ts));
140 }
141 let fmts = [
143 "%Y-%m-%d %H:%M:%S",
144 "%Y-%m-%d %H:%M",
145 "%Y/%m/%d %H:%M:%S",
146 "%Y-%m-%dT%H:%M:%S",
147 ];
148 for fmt in &fmts {
149 if let Ok(dt) = jiff::civil::DateTime::strptime(fmt, s) {
150 let zoned = civil_to_zoned(dt, tz)?;
151 return Ok(timestamp_to_secs(&zoned.timestamp()));
152 }
153 }
154 if let Ok(d) = parse_date_string(s) {
156 let dt = d.at(0, 0, 0, 0);
157 let zoned = civil_to_zoned(dt, tz)?;
158 return Ok(timestamp_to_secs(&zoned.timestamp()));
159 }
160 Err(DateTimeError::AmbiguousFormat)
161}
162
163fn civil_to_zoned(
165 dt: jiff::civil::DateTime,
166 tz: Option<&str>,
167) -> Result<jiff::Zoned, DateTimeError> {
168 let tz_name = tz.unwrap_or("UTC");
169 let tz_obj = jiff::tz::TimeZone::get(tz_name).map_err(|_| DateTimeError::InvalidFormat {
170 reason: format!("unknown timezone '{tz_name}'"),
171 })?;
172 dt.to_zoned(tz_obj)
173 .map_err(|e| DateTimeError::InvalidFormat {
174 reason: e.to_string(),
175 })
176}
177
178fn r_date_vec(days: Vec<Option<f64>>) -> RValue {
180 let mut rv = RVector::from(Vector::Double(days.into()));
181 rv.set_attr(
182 "class".to_string(),
183 RValue::vec(Vector::Character(vec![Some("Date".to_string())].into())),
184 );
185 rv.into()
186}
187
188#[builtin(name = "Sys.Date")]
196fn builtin_sys_date(_args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
197 let today = jiff::Zoned::now().date();
198 Ok(r_date(date_to_days(today)))
199}
200
201#[builtin(name = "Sys.time")]
205fn builtin_sys_time(_args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
206 let ts = Timestamp::now();
207 Ok(r_posixct(timestamp_to_secs(&ts), None))
208}
209
210#[builtin(name = "date")]
217fn builtin_date(_args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
218 let now = jiff::Zoned::now();
219 let formatted = now.strftime("%a %b %d %H:%M:%S %Y").to_string();
221 Ok(RValue::vec(Vector::Character(vec![Some(formatted)].into())))
222}
223
224#[interpreter_builtin(name = "print.Date", min_args = 1)]
233fn interp_print_date(
234 args: &[RValue],
235 named: &[(String, RValue)],
236 context: &BuiltinContext,
237) -> Result<RValue, RError> {
238 let formatted = builtin_format_date(args, named)?;
239 context.write(&format!("{}\n", formatted));
240 Ok(args.first().cloned().unwrap_or(RValue::Null))
241}
242
243#[interpreter_builtin(name = "print.POSIXct", min_args = 1)]
248fn interp_print_posixct(
249 args: &[RValue],
250 named: &[(String, RValue)],
251 context: &BuiltinContext,
252) -> Result<RValue, RError> {
253 let formatted = builtin_format_posixct(args, named)?;
254 context.write(&format!("{}\n", formatted));
255 Ok(args.first().cloned().unwrap_or(RValue::Null))
256}
257
258#[builtin(name = "as.Date", min_args = 1)]
269fn builtin_as_date(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
270 let x = &args[0];
271
272 let format = named
274 .iter()
275 .find(|(k, _)| k == "format")
276 .map(|(_, v)| v)
277 .or(args.get(1))
278 .and_then(|v| v.as_vector())
279 .and_then(|v| v.as_character_scalar());
280
281 let origin = named
283 .iter()
284 .find(|(k, _)| k == "origin")
285 .map(|(_, v)| v)
286 .or(args.get(1));
287
288 match x {
289 RValue::Vector(rv) => {
290 if let Some(cls) = rv.get_attr("class") {
292 if let Some(c) = cls.as_vector().and_then(|v| v.as_character_scalar()) {
293 if c == "Date" {
294 return Ok(x.clone());
295 }
296 }
297 }
298
299 match &rv.inner {
300 Vector::Character(cv) => {
301 let days: Vec<Option<f64>> = cv
303 .iter()
304 .map(|opt_s| {
305 opt_s
306 .as_ref()
307 .map(|s| {
308 if let Some(ref fmt) = format {
309 let jiff_fmt = translate_format(fmt);
310 Date::strptime(&jiff_fmt, s)
311 .map(date_to_days)
312 .map_err(|_| DateTimeError::AmbiguousFormat)
313 } else {
314 parse_date_string(s).map(date_to_days)
315 }
316 })
317 .transpose()
318 })
319 .collect::<Result<_, _>>()?;
320 Ok(r_date_vec(days))
321 }
322 Vector::Double(dv) => {
323 let origin_days = resolve_origin(origin)?;
325 let days: Vec<Option<f64>> = dv
326 .iter()
327 .map(|opt_d| opt_d.map(|d| d + origin_days))
328 .collect();
329 Ok(r_date_vec(days))
330 }
331 Vector::Integer(iv) => {
332 let origin_days = resolve_origin(origin)?;
334 let days: Vec<Option<f64>> = iv
335 .iter_opt()
336 .map(|opt_i| opt_i.map(|i| i as f64 + origin_days))
337 .collect();
338 Ok(r_date_vec(days))
339 }
340 _ => Err(RError::new(
341 RErrorKind::Type,
342 format!(
343 "expected character or numeric, got {}",
344 rv.inner.type_name()
345 ),
346 )),
347 }
348 }
349 _ => Err(RError::new(
350 RErrorKind::Type,
351 format!("expected character or numeric, got {}", x.type_name()),
352 )),
353 }
354}
355
356fn resolve_origin(origin: Option<&RValue>) -> Result<f64, RError> {
358 if let Some(orig) = origin {
359 if let Some(s) = orig.as_vector().and_then(|v| v.as_character_scalar()) {
360 let d = parse_date_string(&s).map_err(|_| DateTimeError::InvalidOrigin)?;
361 Ok(date_to_days(d))
362 } else if let Some(d) = orig.as_vector().and_then(|v| v.as_double_scalar()) {
363 Ok(d)
364 } else {
365 Err(DateTimeError::InvalidOrigin.into())
366 }
367 } else {
368 Err(RError::other(
369 "'origin' must be supplied for numeric conversion",
370 ))
371 }
372}
373
374#[builtin(name = "as.POSIXct", min_args = 1)]
384fn builtin_as_posixct(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
385 let x = &args[0];
386
387 let tz = named
388 .iter()
389 .find(|(k, _)| k == "tz")
390 .map(|(_, v)| v)
391 .and_then(|v| v.as_vector())
392 .and_then(|v| v.as_character_scalar());
393
394 match x {
395 RValue::Vector(rv) => {
396 if let Some(cls) = rv.get_attr("class") {
398 if let Some(c) = cls.as_vector().and_then(|v| v.as_character_scalar()) {
399 if c == "POSIXct" {
400 return Ok(x.clone());
401 }
402 }
403 }
404
405 match &rv.inner {
406 Vector::Character(cv) => {
407 let secs: Vec<Option<f64>> = cv
408 .iter()
409 .map(|opt_s| {
410 opt_s
411 .as_ref()
412 .map(|s| parse_datetime_string(s, tz.as_deref()))
413 .transpose()
414 })
415 .collect::<Result<_, _>>()?;
416 Ok(r_posixct_vec(secs, tz.as_deref()))
417 }
418 Vector::Double(dv) => {
419 let secs: Vec<Option<f64>> = dv.iter_opt().collect();
421 Ok(r_posixct_vec(secs, tz.as_deref()))
422 }
423 _ => Err(RError::new(
424 RErrorKind::Type,
425 format!(
426 "expected character or numeric, got {}",
427 rv.inner.type_name()
428 ),
429 )),
430 }
431 }
432 _ => Err(RError::new(
433 RErrorKind::Type,
434 format!("expected character or numeric, got {}", x.type_name()),
435 )),
436 }
437}
438
439#[builtin(name = "format.Date", min_args = 1)]
449fn builtin_format_date(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
450 let x = &args[0];
451 let format = named
452 .iter()
453 .find(|(k, _)| k == "format")
454 .map(|(_, v)| v)
455 .or(args.get(1))
456 .and_then(|v| v.as_vector())
457 .and_then(|v| v.as_character_scalar())
458 .unwrap_or_else(|| "%Y-%m-%d".to_string());
459
460 let jiff_fmt = translate_format(&format);
461
462 match x {
463 RValue::Vector(rv) => match &rv.inner {
464 Vector::Double(vals) => {
465 let result: Vec<Option<String>> = vals
466 .iter_opt()
467 .map(|opt_d| {
468 opt_d.and_then(|d| {
469 days_to_date(d).map(|date| date.strftime(&jiff_fmt).to_string())
470 })
471 })
472 .collect();
473 Ok(RValue::vec(Vector::Character(result.into())))
474 }
475 _ => Err(RError::new(
476 RErrorKind::Type,
477 format!("expected Date (numeric), got {}", rv.inner.type_name()),
478 )),
479 },
480 _ => Err(RError::new(
481 RErrorKind::Type,
482 format!("expected Date, got {}", x.type_name()),
483 )),
484 }
485}
486
487#[builtin(name = "format.POSIXct", min_args = 1)]
493fn builtin_format_posixct(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
494 let x = &args[0];
495 let format = named
496 .iter()
497 .find(|(k, _)| k == "format")
498 .map(|(_, v)| v)
499 .or(args.get(1))
500 .and_then(|v| v.as_vector())
501 .and_then(|v| v.as_character_scalar())
502 .unwrap_or_else(|| "%Y-%m-%d %H:%M:%S".to_string());
503
504 let jiff_fmt = translate_format(&format);
505
506 let tz_name = if let RValue::Vector(rv) = x {
508 rv.get_attr("tzone")
509 .and_then(|v| v.as_vector())
510 .and_then(|v| v.as_character_scalar())
511 } else {
512 None
513 };
514
515 let tz = if let Some(ref tz_name) = tz_name {
516 jiff::tz::TimeZone::get(tz_name).unwrap_or(jiff::tz::TimeZone::UTC)
517 } else {
518 jiff::tz::TimeZone::system()
519 };
520
521 match x {
522 RValue::Vector(rv) => match &rv.inner {
523 Vector::Double(vals) => {
524 let result: Vec<Option<String>> = vals
525 .iter_opt()
526 .map(|opt_d| {
527 opt_d.and_then(|secs| {
528 secs_to_timestamp(secs)
529 .map(|ts| ts.to_zoned(tz.clone()).strftime(&jiff_fmt).to_string())
530 })
531 })
532 .collect();
533 Ok(RValue::vec(Vector::Character(result.into())))
534 }
535 _ => Err(RError::new(
536 RErrorKind::Type,
537 format!("expected POSIXct (numeric), got {}", rv.inner.type_name()),
538 )),
539 },
540 _ => Err(RError::new(
541 RErrorKind::Type,
542 format!("expected POSIXct, got {}", x.type_name()),
543 )),
544 }
545}
546
547fn secs_to_timestamp(secs: f64) -> Option<Timestamp> {
549 let whole = secs.floor() as i64; let nanos = ((secs - secs.floor()) * 1e9) as i32;
551 Timestamp::new(whole, nanos).ok()
552}
553
554#[builtin(min_args = 2)]
565fn builtin_strptime(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
566 let x = args[0]
567 .as_vector()
568 .and_then(|v| v.as_character_scalar())
569 .ok_or_else(|| {
570 RError::new(
571 RErrorKind::Type,
572 format!("expected character, got {}", args[0].type_name()),
573 )
574 })?;
575
576 let format = named
577 .iter()
578 .find(|(k, _)| k == "format")
579 .map(|(_, v)| v)
580 .or(args.get(1))
581 .and_then(|v| v.as_vector())
582 .and_then(|v| v.as_character_scalar())
583 .ok_or_else(|| RError::new(RErrorKind::Type, "expected character format".to_string()))?;
584
585 let tz = named
586 .iter()
587 .find(|(k, _)| k == "tz")
588 .map(|(_, v)| v)
589 .and_then(|v| v.as_vector())
590 .and_then(|v| v.as_character_scalar());
591
592 let jiff_fmt = translate_format(&format);
593
594 if let Ok(dt) = jiff::civil::DateTime::strptime(&jiff_fmt, &x) {
596 let zoned = civil_to_zoned(dt, tz.as_deref())?;
597 let secs = timestamp_to_secs(&zoned.timestamp());
598 return Ok(r_posixct(secs, tz.as_deref()));
599 }
600
601 if let Ok(d) = Date::strptime(&jiff_fmt, &x) {
602 let dt = d.at(0, 0, 0, 0);
603 let zoned = civil_to_zoned(dt, tz.as_deref())?;
604 let secs = timestamp_to_secs(&zoned.timestamp());
605 return Ok(r_posixct(secs, tz.as_deref()));
606 }
607
608 Err(DateTimeError::AmbiguousFormat.into())
609}
610
611#[builtin(min_args = 1)]
617fn builtin_strftime(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
618 let format = named
620 .iter()
621 .find(|(k, _)| k == "format")
622 .map(|(_, v)| v)
623 .or(args.get(1))
624 .and_then(|v| v.as_vector())
625 .and_then(|v| v.as_character_scalar())
626 .unwrap_or_else(|| "%Y-%m-%d %H:%M:%S".to_string());
627
628 let named_with_format: Vec<(String, RValue)> = vec![(
629 "format".to_string(),
630 RValue::vec(Vector::Character(vec![Some(format)].into())),
631 )];
632 builtin_format_posixct(args, &named_with_format)
633}
634
635#[builtin(min_args = 2)]
646fn builtin_difftime(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
647 let t1 = args[0]
648 .as_vector()
649 .and_then(|v| v.as_double_scalar())
650 .ok_or_else(|| {
651 RError::new(
652 RErrorKind::Type,
653 format!("expected numeric time, got {}", args[0].type_name()),
654 )
655 })?;
656 let t2 = args[1]
657 .as_vector()
658 .and_then(|v| v.as_double_scalar())
659 .ok_or_else(|| {
660 RError::new(
661 RErrorKind::Type,
662 format!("expected numeric time, got {}", args[1].type_name()),
663 )
664 })?;
665
666 let units = named
667 .iter()
668 .find(|(k, _)| k == "units")
669 .map(|(_, v)| v)
670 .or(args.get(2))
671 .and_then(|v| v.as_vector())
672 .and_then(|v| v.as_character_scalar())
673 .unwrap_or_else(|| "secs".to_string());
674
675 let diff_secs = t1 - t2;
676 let value = match units.as_str() {
677 "secs" => diff_secs,
678 "mins" => diff_secs / 60.0,
679 "hours" => diff_secs / 3600.0,
680 "days" => diff_secs / 86400.0,
681 "weeks" => diff_secs / 604800.0,
682 _ => {
683 return Err(RError::other(format!(
684 "invalid 'units' argument: '{units}'"
685 )))
686 }
687 };
688
689 let mut rv = RVector::from(Vector::Double(vec![Some(value)].into()));
690 rv.set_attr(
691 "class".to_string(),
692 RValue::vec(Vector::Character(vec![Some("difftime".to_string())].into())),
693 );
694 rv.set_attr(
695 "units".to_string(),
696 RValue::vec(Vector::Character(vec![Some(units)].into())),
697 );
698 Ok(rv.into())
699}
700
701#[interpreter_builtin(name = "print.difftime", min_args = 1)]
708fn interp_print_difftime(
709 args: &[RValue],
710 _named: &[(String, RValue)],
711 context: &BuiltinContext,
712) -> Result<RValue, RError> {
713 let val = args
714 .first()
715 .ok_or_else(|| RError::new(RErrorKind::Argument, "argument is missing".to_string()))?;
716 let rv = match val {
717 RValue::Vector(rv) => rv,
718 other => {
719 return Err(RError::new(
720 RErrorKind::Type,
721 format!("expected difftime vector, got {}", other.type_name()),
722 ))
723 }
724 };
725 let num = rv.inner.as_double_scalar().unwrap_or(0.0);
726 let units = rv
727 .get_attr("units")
728 .and_then(|v| v.as_vector()?.as_character_scalar())
729 .unwrap_or_else(|| "secs".to_string());
730 context.write(&format!("Time difference of {} {}\n", num, units));
731 context.interpreter().set_invisible();
732 Ok(val.clone())
733}
734
735#[builtin(min_args = 1)]
744fn builtin_weekdays(args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
745 extract_date_component(&args[0], |date| {
746 let name = match date.weekday() {
747 jiff::civil::Weekday::Monday => "Monday",
748 jiff::civil::Weekday::Tuesday => "Tuesday",
749 jiff::civil::Weekday::Wednesday => "Wednesday",
750 jiff::civil::Weekday::Thursday => "Thursday",
751 jiff::civil::Weekday::Friday => "Friday",
752 jiff::civil::Weekday::Saturday => "Saturday",
753 jiff::civil::Weekday::Sunday => "Sunday",
754 };
755 Some(name.to_string())
756 })
757}
758
759#[builtin(min_args = 1)]
764fn builtin_months(args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
765 extract_date_component(&args[0], |date| {
766 let name = match date.month() {
767 1 => "January",
768 2 => "February",
769 3 => "March",
770 4 => "April",
771 5 => "May",
772 6 => "June",
773 7 => "July",
774 8 => "August",
775 9 => "September",
776 10 => "October",
777 11 => "November",
778 12 => "December",
779 _ => unreachable!(),
780 };
781 Some(name.to_string())
782 })
783}
784
785#[builtin(min_args = 1)]
790fn builtin_quarters(args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
791 extract_date_component(&args[0], |date| {
792 let q = (date.month() - 1) / 3 + 1;
793 Some(format!("Q{q}"))
794 })
795}
796
797fn extract_date_component(
799 x: &RValue,
800 f: impl Fn(Date) -> Option<String>,
801) -> Result<RValue, RError> {
802 match x {
803 RValue::Vector(rv) => match &rv.inner {
804 Vector::Double(vals) => {
805 let result: Vec<Option<String>> = vals
806 .iter_opt()
807 .map(|opt_d| opt_d.and_then(|d| days_to_date(d).and_then(&f)))
808 .collect();
809 Ok(RValue::vec(Vector::Character(result.into())))
810 }
811 _ => Err(RError::new(
812 RErrorKind::Type,
813 format!("expected Date (numeric), got {}", rv.inner.type_name()),
814 )),
815 },
816 _ => Err(RError::new(
817 RErrorKind::Type,
818 format!("expected Date, got {}", x.type_name()),
819 )),
820 }
821}
822
823#[builtin(name = "as.POSIXlt", min_args = 1)]
833fn builtin_as_posixlt(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
834 let x = &args[0];
835 let tz = named
836 .iter()
837 .find(|(k, _)| k == "tz")
838 .map(|(_, v)| v)
839 .and_then(|v| v.as_vector())
840 .and_then(|v| v.as_character_scalar());
841
842 let secs = match x {
844 RValue::Vector(rv) => {
845 if let Some(d) = rv.inner.as_double_scalar() {
846 d
847 } else if let Some(s) = rv.inner.as_character_scalar() {
848 parse_datetime_string(&s, tz.as_deref())?
849 } else {
850 return Err(RError::new(
851 RErrorKind::Type,
852 format!(
853 "expected character or numeric, got {}",
854 rv.inner.type_name()
855 ),
856 ));
857 }
858 }
859 _ => {
860 return Err(RError::new(
861 RErrorKind::Type,
862 format!("expected character or numeric, got {}", x.type_name()),
863 ));
864 }
865 };
866
867 let tz_obj = if let Some(ref tz_name) = tz {
868 jiff::tz::TimeZone::get(tz_name).unwrap_or(jiff::tz::TimeZone::UTC)
869 } else {
870 jiff::tz::TimeZone::system()
871 };
872
873 let ts = secs_to_timestamp(secs).ok_or_else(|| DateTimeError::InvalidFormat {
874 reason: "timestamp out of range".to_string(),
875 })?;
876 let zoned = ts.to_zoned(tz_obj);
877 let dt = zoned.datetime();
878
879 let wday = match dt.date().weekday() {
882 jiff::civil::Weekday::Sunday => 0i64,
883 jiff::civil::Weekday::Monday => 1,
884 jiff::civil::Weekday::Tuesday => 2,
885 jiff::civil::Weekday::Wednesday => 3,
886 jiff::civil::Weekday::Thursday => 4,
887 jiff::civil::Weekday::Friday => 5,
888 jiff::civil::Weekday::Saturday => 6,
889 };
890
891 let yday = i64::from(dt.date().day_of_year()) - 1; let components: Vec<(Option<String>, RValue)> = vec![
894 (
895 Some("sec".to_string()),
896 RValue::vec(Vector::Double(
897 vec![Some(
898 f64::from(dt.time().second()) + f64::from(dt.time().subsec_nanosecond()) / 1e9,
899 )]
900 .into(),
901 )),
902 ),
903 (
904 Some("min".to_string()),
905 RValue::vec(Vector::Integer(
906 vec![Some(i64::from(dt.time().minute()))].into(),
907 )),
908 ),
909 (
910 Some("hour".to_string()),
911 RValue::vec(Vector::Integer(
912 vec![Some(i64::from(dt.time().hour()))].into(),
913 )),
914 ),
915 (
916 Some("mday".to_string()),
917 RValue::vec(Vector::Integer(
918 vec![Some(i64::from(dt.date().day()))].into(),
919 )),
920 ),
921 (
922 Some("mon".to_string()),
923 RValue::vec(Vector::Integer(
924 vec![Some(i64::from(dt.date().month()) - 1)].into(),
925 )),
926 ),
927 (
928 Some("year".to_string()),
929 RValue::vec(Vector::Integer(
930 vec![Some(i64::from(dt.date().year()) - 1900)].into(),
931 )),
932 ),
933 (
934 Some("wday".to_string()),
935 RValue::vec(Vector::Integer(vec![Some(wday)].into())),
936 ),
937 (
938 Some("yday".to_string()),
939 RValue::vec(Vector::Integer(vec![Some(yday)].into())),
940 ),
941 (
942 Some("isdst".to_string()),
943 RValue::vec(Vector::Integer(vec![Some(-1i64)].into())), ),
945 ];
946
947 let mut list = RList::new(components);
948 list.set_attr(
949 "class".to_string(),
950 RValue::vec(Vector::Character(
951 vec![Some("POSIXlt".to_string()), Some("POSIXt".to_string())].into(),
952 )),
953 );
954 Ok(list.into())
955}
956
957