1use std::cell::Cell;
2use std::collections::{HashMap, HashSet};
3
4use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, Timelike, Weekday};
5use runmat_builtins::{
6 Access, BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
7 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
8 CharArray, ClassDef, MethodDef, ObjectInstance, PropertyDef, StringArray, Tensor, Value,
9};
10
11use crate::builtins::common::tensor;
12use crate::{
13 build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError, OBJECT_INDEX_MEMBER,
14 OBJECT_INDEX_PAREN, OBJECT_SUBSASGN_METHOD, OBJECT_SUBSREF_METHOD,
15};
16
17const BUILTIN_NAME: &str = "datetime";
18const DATETIME_CLASS: &str = "datetime";
19const CALENDAR_DURATION_CLASS: &str = "calendarDuration";
20const SERIAL_FIELD: &str = "__serial";
21const CALENDAR_MONTHS_FIELD: &str = "__months";
22const CALENDAR_DAYS_FIELD: &str = "__days";
23const FORMAT_FIELD: &str = "Format";
24const DEFAULT_DATE_FORMAT: &str = "dd-MMM-yyyy";
25const DEFAULT_DATETIME_FORMAT: &str = "dd-MMM-yyyy HH:mm:ss";
26const UNIX_DATENUM: f64 = 719_529.0;
27const SECONDS_PER_DAY: f64 = 86_400.0;
28const MAX_HOLIDAY_YEAR_SPAN: i32 = 1_000;
29const MAX_BUSDAYS_OUTPUT_LEN: i64 = 1_000_000;
30
31type Broadcast3 = (Vec<f64>, Vec<f64>, Vec<f64>, Vec<usize>);
32
33thread_local! {
34 static DATETIME_CLASS_REGISTERED: Cell<bool> = const { Cell::new(false) };
35 static CALENDAR_DURATION_CLASS_REGISTERED: Cell<bool> = const { Cell::new(false) };
36}
37
38const DATETIME_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
39 code: "RM.DATETIME.INVALID_ARGUMENT",
40 identifier: Some("RunMat:datetime:InvalidArgument"),
41 when: "Arguments or option grammar do not match supported datetime forms.",
42 message: "datetime: invalid argument",
43};
44const DATETIME_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
45 code: "RM.DATETIME.INVALID_INPUT",
46 identifier: Some("RunMat:datetime:InvalidInput"),
47 when: "Input values cannot be parsed/converted/broadcast to a valid datetime result.",
48 message: "datetime: invalid input",
49};
50const DATETIME_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
51 code: "RM.DATETIME.INTERNAL",
52 identifier: Some("RunMat:datetime:Internal"),
53 when: "Internal datetime state or indexing/evaluation failed unexpectedly.",
54 message: "datetime: internal operation failed",
55};
56const DATETIME_ERRORS: [BuiltinErrorDescriptor; 3] = [
57 DATETIME_ERROR_INVALID_ARGUMENT,
58 DATETIME_ERROR_INVALID_INPUT,
59 DATETIME_ERROR_INTERNAL,
60];
61
62const OUT_DATETIME: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
63 name: "t",
64 ty: BuiltinParamType::Any,
65 arity: BuiltinParamArity::Required,
66 default: None,
67 description: "Datetime object result.",
68}];
69const OUT_NUMERIC: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
70 name: "X",
71 ty: BuiltinParamType::Any,
72 arity: BuiltinParamArity::Required,
73 default: None,
74 description: "Numeric scalar/tensor result.",
75}];
76const OUT_ANY: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
77 name: "out",
78 ty: BuiltinParamType::Any,
79 arity: BuiltinParamArity::Required,
80 default: None,
81 description: "Method result.",
82}];
83const DATETIME_ARGS_ONLY: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
84 name: "args",
85 ty: BuiltinParamType::Any,
86 arity: BuiltinParamArity::Variadic,
87 default: None,
88 description: "Datetime constructor arguments.",
89}];
90const DATETIME_SINGLE_INPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
91 name: "value",
92 ty: BuiltinParamType::Any,
93 arity: BuiltinParamArity::Required,
94 default: None,
95 description: "Datetime input.",
96}];
97const DATETIME_BINARY_INPUTS: [BuiltinParamDescriptor; 2] = [
98 BuiltinParamDescriptor {
99 name: "lhs",
100 ty: BuiltinParamType::Any,
101 arity: BuiltinParamArity::Required,
102 default: None,
103 description: "Left datetime operand.",
104 },
105 BuiltinParamDescriptor {
106 name: "rhs",
107 ty: BuiltinParamType::Any,
108 arity: BuiltinParamArity::Required,
109 default: None,
110 description: "Right datetime/numeric/duration operand.",
111 },
112];
113const DATESHIFT_INPUTS: [BuiltinParamDescriptor; 4] = [
114 BuiltinParamDescriptor {
115 name: "t",
116 ty: BuiltinParamType::Any,
117 arity: BuiltinParamArity::Required,
118 default: None,
119 description: "Datetime input.",
120 },
121 BuiltinParamDescriptor {
122 name: "boundary",
123 ty: BuiltinParamType::StringScalar,
124 arity: BuiltinParamArity::Required,
125 default: None,
126 description: "Shift boundary: 'start', 'end', or 'nearest'.",
127 },
128 BuiltinParamDescriptor {
129 name: "unit",
130 ty: BuiltinParamType::StringScalar,
131 arity: BuiltinParamArity::Required,
132 default: None,
133 description: "Calendar/time unit.",
134 },
135 BuiltinParamDescriptor {
136 name: "weekdayOrOption",
137 ty: BuiltinParamType::Any,
138 arity: BuiltinParamArity::Optional,
139 default: None,
140 description: "Optional weekday for week-based shifts.",
141 },
142];
143const DATETIME_SUBSREF_INPUTS: [BuiltinParamDescriptor; 3] = [
144 BuiltinParamDescriptor {
145 name: "obj",
146 ty: BuiltinParamType::Any,
147 arity: BuiltinParamArity::Required,
148 default: None,
149 description: "Datetime receiver object.",
150 },
151 BuiltinParamDescriptor {
152 name: "kind",
153 ty: BuiltinParamType::StringScalar,
154 arity: BuiltinParamArity::Required,
155 default: None,
156 description: "Indexing kind token.",
157 },
158 BuiltinParamDescriptor {
159 name: "payload",
160 ty: BuiltinParamType::Any,
161 arity: BuiltinParamArity::Required,
162 default: None,
163 description: "Index/member payload.",
164 },
165];
166const DATETIME_SUBSASGN_INPUTS: [BuiltinParamDescriptor; 4] = [
167 BuiltinParamDescriptor {
168 name: "obj",
169 ty: BuiltinParamType::Any,
170 arity: BuiltinParamArity::Required,
171 default: None,
172 description: "Datetime receiver object.",
173 },
174 BuiltinParamDescriptor {
175 name: "kind",
176 ty: BuiltinParamType::StringScalar,
177 arity: BuiltinParamArity::Required,
178 default: None,
179 description: "Indexing kind token.",
180 },
181 BuiltinParamDescriptor {
182 name: "payload",
183 ty: BuiltinParamType::Any,
184 arity: BuiltinParamArity::Required,
185 default: None,
186 description: "Index/member payload.",
187 },
188 BuiltinParamDescriptor {
189 name: "rhs",
190 ty: BuiltinParamType::Any,
191 arity: BuiltinParamArity::Required,
192 default: None,
193 description: "Assigned value.",
194 },
195];
196
197const DATETIME_SIGNATURES: [BuiltinSignatureDescriptor; 11] = [
198 BuiltinSignatureDescriptor {
199 label: "t = datetime()",
200 inputs: &[],
201 outputs: &OUT_DATETIME,
202 },
203 BuiltinSignatureDescriptor {
204 label: "t = datetime(textOrArray)",
205 inputs: &[BuiltinParamDescriptor {
206 name: "textOrArray",
207 ty: BuiltinParamType::Any,
208 arity: BuiltinParamArity::Required,
209 default: None,
210 description: "String/char/date text input.",
211 }],
212 outputs: &OUT_DATETIME,
213 },
214 BuiltinSignatureDescriptor {
215 label: "t = datetime(serialDateNumbers)",
216 inputs: &[BuiltinParamDescriptor {
217 name: "serialDateNumbers",
218 ty: BuiltinParamType::NumericArray,
219 arity: BuiltinParamArity::Required,
220 default: None,
221 description: "Numeric serial date input.",
222 }],
223 outputs: &OUT_DATETIME,
224 },
225 BuiltinSignatureDescriptor {
226 label: "t = datetime(year, month, day)",
227 inputs: &[
228 BuiltinParamDescriptor {
229 name: "year",
230 ty: BuiltinParamType::NumericArray,
231 arity: BuiltinParamArity::Required,
232 default: None,
233 description: "Year component.",
234 },
235 BuiltinParamDescriptor {
236 name: "month",
237 ty: BuiltinParamType::NumericArray,
238 arity: BuiltinParamArity::Required,
239 default: None,
240 description: "Month component.",
241 },
242 BuiltinParamDescriptor {
243 name: "day",
244 ty: BuiltinParamType::NumericArray,
245 arity: BuiltinParamArity::Required,
246 default: None,
247 description: "Day component.",
248 },
249 ],
250 outputs: &OUT_DATETIME,
251 },
252 BuiltinSignatureDescriptor {
253 label: "t = datetime(year, month, day, hour)",
254 inputs: &[
255 BuiltinParamDescriptor {
256 name: "year",
257 ty: BuiltinParamType::NumericArray,
258 arity: BuiltinParamArity::Required,
259 default: None,
260 description: "Year component.",
261 },
262 BuiltinParamDescriptor {
263 name: "month",
264 ty: BuiltinParamType::NumericArray,
265 arity: BuiltinParamArity::Required,
266 default: None,
267 description: "Month component.",
268 },
269 BuiltinParamDescriptor {
270 name: "day",
271 ty: BuiltinParamType::NumericArray,
272 arity: BuiltinParamArity::Required,
273 default: None,
274 description: "Day component.",
275 },
276 BuiltinParamDescriptor {
277 name: "hour",
278 ty: BuiltinParamType::NumericArray,
279 arity: BuiltinParamArity::Required,
280 default: None,
281 description: "Hour component.",
282 },
283 ],
284 outputs: &OUT_DATETIME,
285 },
286 BuiltinSignatureDescriptor {
287 label: "t = datetime(year, month, day, hour, minute)",
288 inputs: &[
289 BuiltinParamDescriptor {
290 name: "year",
291 ty: BuiltinParamType::NumericArray,
292 arity: BuiltinParamArity::Required,
293 default: None,
294 description: "Year component.",
295 },
296 BuiltinParamDescriptor {
297 name: "month",
298 ty: BuiltinParamType::NumericArray,
299 arity: BuiltinParamArity::Required,
300 default: None,
301 description: "Month component.",
302 },
303 BuiltinParamDescriptor {
304 name: "day",
305 ty: BuiltinParamType::NumericArray,
306 arity: BuiltinParamArity::Required,
307 default: None,
308 description: "Day component.",
309 },
310 BuiltinParamDescriptor {
311 name: "hour",
312 ty: BuiltinParamType::NumericArray,
313 arity: BuiltinParamArity::Required,
314 default: None,
315 description: "Hour component.",
316 },
317 BuiltinParamDescriptor {
318 name: "minute",
319 ty: BuiltinParamType::NumericArray,
320 arity: BuiltinParamArity::Required,
321 default: None,
322 description: "Minute component.",
323 },
324 ],
325 outputs: &OUT_DATETIME,
326 },
327 BuiltinSignatureDescriptor {
328 label: "t = datetime(year, month, day, hour, minute, second)",
329 inputs: &[
330 BuiltinParamDescriptor {
331 name: "year",
332 ty: BuiltinParamType::NumericArray,
333 arity: BuiltinParamArity::Required,
334 default: None,
335 description: "Year component.",
336 },
337 BuiltinParamDescriptor {
338 name: "month",
339 ty: BuiltinParamType::NumericArray,
340 arity: BuiltinParamArity::Required,
341 default: None,
342 description: "Month component.",
343 },
344 BuiltinParamDescriptor {
345 name: "day",
346 ty: BuiltinParamType::NumericArray,
347 arity: BuiltinParamArity::Required,
348 default: None,
349 description: "Day component.",
350 },
351 BuiltinParamDescriptor {
352 name: "hour",
353 ty: BuiltinParamType::NumericArray,
354 arity: BuiltinParamArity::Required,
355 default: None,
356 description: "Hour component.",
357 },
358 BuiltinParamDescriptor {
359 name: "minute",
360 ty: BuiltinParamType::NumericArray,
361 arity: BuiltinParamArity::Required,
362 default: None,
363 description: "Minute component.",
364 },
365 BuiltinParamDescriptor {
366 name: "second",
367 ty: BuiltinParamType::NumericArray,
368 arity: BuiltinParamArity::Required,
369 default: None,
370 description: "Second component.",
371 },
372 ],
373 outputs: &OUT_DATETIME,
374 },
375 BuiltinSignatureDescriptor {
376 label: "t = datetime(serialDateNumbers, \"ConvertFrom\", \"datenum\")",
377 inputs: &[BuiltinParamDescriptor {
378 name: "args",
379 ty: BuiltinParamType::Any,
380 arity: BuiltinParamArity::Variadic,
381 default: None,
382 description: "Numeric serial input with ConvertFrom option.",
383 }],
384 outputs: &OUT_DATETIME,
385 },
386 BuiltinSignatureDescriptor {
387 label: "t = datetime(___, \"Format\", format)",
388 inputs: &DATETIME_ARGS_ONLY,
389 outputs: &OUT_DATETIME,
390 },
391 BuiltinSignatureDescriptor {
392 label: "t = datetime(textOrArray, \"InputFormat\", inputFormat)",
393 inputs: &DATETIME_ARGS_ONLY,
394 outputs: &OUT_DATETIME,
395 },
396 BuiltinSignatureDescriptor {
397 label: "t = datetime(___, Name, Value, ...)",
398 inputs: &DATETIME_ARGS_ONLY,
399 outputs: &OUT_DATETIME,
400 },
401];
402
403const DATETIME_YEAR_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
404 label: "X = year(t)",
405 inputs: &DATETIME_SINGLE_INPUT,
406 outputs: &OUT_NUMERIC,
407}];
408const DATETIME_MONTH_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
409 label: "X = month(t)",
410 inputs: &DATETIME_SINGLE_INPUT,
411 outputs: &OUT_NUMERIC,
412}];
413const DATETIME_DAY_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
414 label: "X = day(t)",
415 inputs: &DATETIME_SINGLE_INPUT,
416 outputs: &OUT_NUMERIC,
417}];
418const DATETIME_HOUR_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
419 label: "X = hour(t)",
420 inputs: &DATETIME_SINGLE_INPUT,
421 outputs: &OUT_NUMERIC,
422}];
423const DATETIME_MINUTE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
424 label: "X = minute(t)",
425 inputs: &DATETIME_SINGLE_INPUT,
426 outputs: &OUT_NUMERIC,
427}];
428const DATETIME_SECOND_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
429 label: "X = second(t)",
430 inputs: &DATETIME_SINGLE_INPUT,
431 outputs: &OUT_NUMERIC,
432}];
433const DATETIME_SUBSREF_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
434 label: "out = datetime.subsref(obj, kind, payload)",
435 inputs: &DATETIME_SUBSREF_INPUTS,
436 outputs: &OUT_ANY,
437}];
438const DATETIME_SUBSASGN_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
439 [BuiltinSignatureDescriptor {
440 label: "out = datetime.subsasgn(obj, kind, payload, rhs)",
441 inputs: &DATETIME_SUBSASGN_INPUTS,
442 outputs: &OUT_ANY,
443 }];
444const DATETIME_BINARY_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
445 label: "out = datetime.op(lhs, rhs)",
446 inputs: &DATETIME_BINARY_INPUTS,
447 outputs: &OUT_ANY,
448}];
449const DATESHIFT_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
450 BuiltinSignatureDescriptor {
451 label: "t2 = dateshift(t, boundary, unit)",
452 inputs: &DATESHIFT_INPUTS,
453 outputs: &OUT_DATETIME,
454 },
455 BuiltinSignatureDescriptor {
456 label: "t2 = dateshift(t, boundary, \"week\", weekday)",
457 inputs: &DATESHIFT_INPUTS,
458 outputs: &OUT_DATETIME,
459 },
460 BuiltinSignatureDescriptor {
461 label: "t2 = dateshift(t, \"dayofweek\", weekday)",
462 inputs: &DATESHIFT_INPUTS,
463 outputs: &OUT_DATETIME,
464 },
465];
466
467pub const DATETIME_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
468 signatures: &DATETIME_SIGNATURES,
469 output_mode: BuiltinOutputMode::Fixed,
470 completion_policy: BuiltinCompletionPolicy::Public,
471 errors: &DATETIME_ERRORS,
472};
473pub const DATETIME_YEAR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
474 signatures: &DATETIME_YEAR_SIGNATURES,
475 output_mode: BuiltinOutputMode::Fixed,
476 completion_policy: BuiltinCompletionPolicy::Public,
477 errors: &DATETIME_ERRORS,
478};
479pub const DATETIME_MONTH_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
480 signatures: &DATETIME_MONTH_SIGNATURES,
481 output_mode: BuiltinOutputMode::Fixed,
482 completion_policy: BuiltinCompletionPolicy::Public,
483 errors: &DATETIME_ERRORS,
484};
485pub const DATETIME_DAY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
486 signatures: &DATETIME_DAY_SIGNATURES,
487 output_mode: BuiltinOutputMode::Fixed,
488 completion_policy: BuiltinCompletionPolicy::Public,
489 errors: &DATETIME_ERRORS,
490};
491pub const DATETIME_HOUR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
492 signatures: &DATETIME_HOUR_SIGNATURES,
493 output_mode: BuiltinOutputMode::Fixed,
494 completion_policy: BuiltinCompletionPolicy::Public,
495 errors: &DATETIME_ERRORS,
496};
497pub const DATETIME_MINUTE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
498 signatures: &DATETIME_MINUTE_SIGNATURES,
499 output_mode: BuiltinOutputMode::Fixed,
500 completion_policy: BuiltinCompletionPolicy::Public,
501 errors: &DATETIME_ERRORS,
502};
503pub const DATETIME_SECOND_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
504 signatures: &DATETIME_SECOND_SIGNATURES,
505 output_mode: BuiltinOutputMode::Fixed,
506 completion_policy: BuiltinCompletionPolicy::Public,
507 errors: &DATETIME_ERRORS,
508};
509pub const DATETIME_SUBSREF_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
510 signatures: &DATETIME_SUBSREF_SIGNATURES,
511 output_mode: BuiltinOutputMode::Fixed,
512 completion_policy: BuiltinCompletionPolicy::MethodOnly,
513 errors: &DATETIME_ERRORS,
514};
515pub const DATETIME_SUBSASGN_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
516 signatures: &DATETIME_SUBSASGN_SIGNATURES,
517 output_mode: BuiltinOutputMode::Fixed,
518 completion_policy: BuiltinCompletionPolicy::MethodOnly,
519 errors: &DATETIME_ERRORS,
520};
521pub const DATETIME_BINARY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
522 signatures: &DATETIME_BINARY_SIGNATURES,
523 output_mode: BuiltinOutputMode::Fixed,
524 completion_policy: BuiltinCompletionPolicy::MethodOnly,
525 errors: &DATETIME_ERRORS,
526};
527pub const DATESHIFT_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
528 signatures: &DATESHIFT_SIGNATURES,
529 output_mode: BuiltinOutputMode::Fixed,
530 completion_policy: BuiltinCompletionPolicy::Public,
531 errors: &DATETIME_ERRORS,
532};
533
534fn datetime_error(message: impl Into<String>) -> RuntimeError {
535 build_runtime_error(message)
536 .with_builtin(BUILTIN_NAME)
537 .build()
538}
539
540fn ensure_datetime_class_registered() {
541 DATETIME_CLASS_REGISTERED.with(|registered| {
542 if registered.get() {
543 return;
544 }
545 let mut properties = HashMap::new();
546 properties.insert(
547 FORMAT_FIELD.to_string(),
548 PropertyDef {
549 name: FORMAT_FIELD.to_string(),
550 is_static: false,
551 is_constant: false,
552 is_dependent: false,
553 get_access: Access::Public,
554 set_access: Access::Public,
555 default_value: Some(Value::String(DEFAULT_DATETIME_FORMAT.to_string())),
556 },
557 );
558
559 let mut methods = HashMap::new();
560 for name in [
561 OBJECT_SUBSREF_METHOD,
562 OBJECT_SUBSASGN_METHOD,
563 "plus",
564 "minus",
565 "eq",
566 "ne",
567 "lt",
568 "le",
569 "gt",
570 "ge",
571 ] {
572 methods.insert(
573 name.to_string(),
574 MethodDef {
575 name: name.to_string(),
576 is_static: false,
577 is_abstract: false,
578 is_sealed: false,
579 access: Access::Public,
580 function_name: format!("{DATETIME_CLASS}.{name}"),
581 implicit_class_argument: None,
582 },
583 );
584 }
585
586 runmat_builtins::register_class(ClassDef {
587 name: DATETIME_CLASS.to_string(),
588 parent: None,
589 properties,
590 methods,
591 });
592 registered.set(true);
593 });
594}
595
596fn ensure_calendar_duration_class_registered() {
597 CALENDAR_DURATION_CLASS_REGISTERED.with(|registered| {
598 if registered.get() {
599 return;
600 }
601
602 let mut properties = HashMap::new();
603 for name in [CALENDAR_MONTHS_FIELD, CALENDAR_DAYS_FIELD] {
604 properties.insert(
605 name.to_string(),
606 PropertyDef {
607 name: name.to_string(),
608 is_static: false,
609 is_constant: false,
610 is_dependent: false,
611 get_access: Access::Public,
612 set_access: Access::Public,
613 default_value: Some(Value::Num(0.0)),
614 },
615 );
616 }
617
618 let mut methods = HashMap::new();
619 for name in ["plus", "minus", "eq", "ne"] {
620 methods.insert(
621 name.to_string(),
622 MethodDef {
623 name: name.to_string(),
624 is_static: false,
625 is_abstract: false,
626 is_sealed: false,
627 access: Access::Public,
628 function_name: format!("{CALENDAR_DURATION_CLASS}.{name}"),
629 implicit_class_argument: None,
630 },
631 );
632 }
633
634 runmat_builtins::register_class(ClassDef {
635 name: CALENDAR_DURATION_CLASS.to_string(),
636 parent: None,
637 properties,
638 methods,
639 });
640 registered.set(true);
641 });
642}
643
644async fn gather_args(args: &[Value]) -> BuiltinResult<Vec<Value>> {
645 let mut out = Vec::with_capacity(args.len());
646 for arg in args {
647 out.push(
648 gather_if_needed_async(arg)
649 .await
650 .map_err(|err| datetime_error(format!("datetime: {}", err.message())))?,
651 );
652 }
653 Ok(out)
654}
655
656fn scalar_text(value: &Value, context: &str) -> BuiltinResult<String> {
657 match value {
658 Value::String(text) => Ok(text.clone()),
659 Value::StringArray(array) if array.data.len() == 1 => Ok(array.data[0].clone()),
660 Value::CharArray(array) if array.rows == 1 => Ok(array.data.iter().collect()),
661 _ => Err(datetime_error(format!(
662 "datetime: {context} must be a string scalar or character vector"
663 ))),
664 }
665}
666
667#[derive(Default)]
668struct DatetimeOptions {
669 format: Option<String>,
670 convert_from: Option<String>,
671 input_format: Option<String>,
672}
673
674fn parse_trailing_options(args: &[Value]) -> BuiltinResult<(usize, DatetimeOptions)> {
675 let mut positional_end = args.len();
676 let mut options = DatetimeOptions::default();
677
678 while positional_end >= 2 {
679 let name = match scalar_text(&args[positional_end - 2], "option name") {
680 Ok(text) => text,
681 Err(_) => break,
682 };
683 let lowered = name.trim().to_ascii_lowercase();
684 let value = scalar_text(&args[positional_end - 1], &format!("{name} option"))?;
685 match lowered.as_str() {
686 "format" => options.format = Some(value),
687 "convertfrom" => options.convert_from = Some(value),
688 "inputformat" => options.input_format = Some(value),
689 _ => break,
690 }
691 positional_end -= 2;
692 }
693
694 Ok((positional_end, options))
695}
696
697fn tensor_from_numeric(value: Value, context: &str) -> BuiltinResult<Tensor> {
698 tensor::value_into_tensor_for(context, value)
699 .map_err(|message| datetime_error(format!("datetime: {message}")))
700}
701
702fn serial_tensor_from_value(value: Value, context: &str) -> BuiltinResult<Tensor> {
703 let tensor = tensor_from_numeric(value, context)?;
704 Tensor::new(
705 tensor.data.clone(),
706 tensor::default_shape_for(&tensor.shape, tensor.data.len()),
707 )
708 .map_err(|err| datetime_error(format!("datetime: {err}")))
709}
710
711fn format_for_object(obj: &ObjectInstance) -> String {
712 match obj.properties.get(FORMAT_FIELD) {
713 Some(Value::String(text)) => text.clone(),
714 Some(Value::StringArray(array)) if array.data.len() == 1 => array.data[0].clone(),
715 Some(Value::CharArray(array)) if array.rows == 1 => array.data.iter().collect(),
716 _ => DEFAULT_DATETIME_FORMAT.to_string(),
717 }
718}
719
720fn serial_tensor_for_object(obj: &ObjectInstance) -> BuiltinResult<Tensor> {
721 match obj.properties.get(SERIAL_FIELD) {
722 Some(Value::Tensor(tensor)) => Ok(tensor.clone()),
723 Some(Value::Num(value)) => Tensor::new(vec![*value], vec![1, 1])
724 .map_err(|err| datetime_error(format!("datetime: {err}"))),
725 Some(other) => Err(datetime_error(format!(
726 "datetime: invalid internal serial storage {other:?}"
727 ))),
728 None => Err(datetime_error("datetime: missing internal serial storage")),
729 }
730}
731
732pub(crate) fn datetime_object_from_serial_tensor(
733 serials: Tensor,
734 format: impl Into<String>,
735) -> BuiltinResult<Value> {
736 ensure_datetime_class_registered();
737 let mut object = ObjectInstance::new(DATETIME_CLASS.to_string());
738 object
739 .properties
740 .insert(SERIAL_FIELD.to_string(), Value::Tensor(serials));
741 object
742 .properties
743 .insert(FORMAT_FIELD.to_string(), Value::String(format.into()));
744 Ok(Value::Object(object))
745}
746
747fn datetime_object_from_serials(
748 serials: Vec<f64>,
749 shape: Vec<usize>,
750 format: impl Into<String>,
751) -> BuiltinResult<Value> {
752 let tensor =
753 Tensor::new(serials, shape).map_err(|err| datetime_error(format!("datetime: {err}")))?;
754 datetime_object_from_serial_tensor(tensor, format)
755}
756
757fn format_token_to_strftime(format: &str) -> String {
758 let mut out = format.to_string();
759 for (src, dst) in [
760 ("yyyy", "%Y"),
761 ("MMM", "%b"),
762 ("MM", "%m"),
763 ("dd", "%d"),
764 ("HH", "%H"),
765 ("mm", "%M"),
766 ("ss", "%S"),
767 ] {
768 out = out.replace(src, dst);
769 }
770 out
771}
772
773pub(crate) fn datenum_from_naive(datetime: NaiveDateTime) -> f64 {
774 let base = NaiveDate::from_ymd_opt(1970, 1, 1)
775 .unwrap()
776 .and_hms_opt(0, 0, 0)
777 .unwrap();
778 let duration = datetime - base;
779 let seconds = duration.num_seconds();
780 let nanos = (duration - Duration::seconds(seconds))
781 .num_nanoseconds()
782 .unwrap_or(0);
783 let total_seconds = seconds as f64 + nanos as f64 / 1_000_000_000.0;
784 total_seconds / SECONDS_PER_DAY + UNIX_DATENUM
785}
786
787fn naive_from_datenum(serial: f64) -> BuiltinResult<NaiveDateTime> {
788 if !serial.is_finite() {
789 return Err(datetime_error(
790 "datetime: serial date numbers must be finite",
791 ));
792 }
793 let total_nanos = ((serial - UNIX_DATENUM) * SECONDS_PER_DAY * 1_000_000_000.0).round() as i128;
794 let seconds = total_nanos.div_euclid(1_000_000_000) as i64;
795 let nanos = total_nanos.rem_euclid(1_000_000_000) as i64;
796 let base = NaiveDate::from_ymd_opt(1970, 1, 1)
797 .unwrap()
798 .and_hms_opt(0, 0, 0)
799 .unwrap();
800 Ok(base + Duration::seconds(seconds) + Duration::nanoseconds(nanos))
801}
802
803fn format_serial(serial: f64, format: &str) -> BuiltinResult<String> {
804 if serial.is_nan() {
805 return Ok("NaT".to_string());
806 }
807 let naive = naive_from_datenum(serial)?;
808 let chrono_format = format_token_to_strftime(format);
809 Ok(naive.format(&chrono_format).to_string())
810}
811
812fn parse_datetime_text(text: &str) -> Option<(NaiveDateTime, bool)> {
813 let trimmed = text.trim();
814 if trimmed.is_empty() {
815 return None;
816 }
817
818 if let Ok(value) = DateTime::parse_from_rfc3339(trimmed) {
819 return Some((value.with_timezone(&Local).naive_local(), true));
820 }
821
822 for (pattern, has_time) in [
823 ("%Y-%m-%d %H:%M:%S", true),
824 ("%Y-%m-%d", false),
825 ("%d-%b-%Y %H:%M:%S", true),
826 ("%d-%b-%Y", false),
827 ("%m/%d/%Y %H:%M:%S", true),
828 ("%m/%d/%Y", false),
829 ] {
830 if has_time {
831 if let Ok(value) = NaiveDateTime::parse_from_str(trimmed, pattern) {
832 return Some((value, true));
833 }
834 } else if let Ok(value) = NaiveDate::parse_from_str(trimmed, pattern) {
835 return Some((value.and_hms_opt(0, 0, 0).unwrap(), false));
836 }
837 }
838
839 None
840}
841
842fn parse_datetime_text_with_input_format(
843 text: &str,
844 input_format: Option<&str>,
845) -> Option<(NaiveDateTime, bool)> {
846 let trimmed = text.trim();
847 if trimmed.is_empty() {
848 return None;
849 }
850 let Some(input_format) = input_format else {
851 return parse_datetime_text(trimmed);
852 };
853 let chrono_format = format_token_to_strftime(input_format);
854 if let Ok(value) = NaiveDateTime::parse_from_str(trimmed, &chrono_format) {
855 return Some((value, true));
856 }
857 if let Ok(value) = NaiveDate::parse_from_str(trimmed, &chrono_format) {
858 return Some((value.and_hms_opt(0, 0, 0).unwrap(), false));
859 }
860 None
861}
862
863fn parse_text_input(
864 value: Value,
865 input_format: Option<&str>,
866) -> BuiltinResult<(Vec<f64>, Vec<usize>, String)> {
867 match value {
868 Value::String(text) => {
869 if text.trim().eq_ignore_ascii_case("now") {
870 let now = Local::now().naive_local();
871 return Ok((
872 vec![datenum_from_naive(now)],
873 vec![1, 1],
874 DEFAULT_DATETIME_FORMAT.to_string(),
875 ));
876 }
877 let (naive, has_time) = parse_datetime_text_with_input_format(&text, input_format)
878 .ok_or_else(|| {
879 datetime_error(format!("datetime: unable to parse date/time text '{text}'"))
880 })?;
881 Ok((
882 vec![datenum_from_naive(naive)],
883 vec![1, 1],
884 if has_time {
885 DEFAULT_DATETIME_FORMAT.to_string()
886 } else {
887 DEFAULT_DATE_FORMAT.to_string()
888 },
889 ))
890 }
891 Value::StringArray(array) => {
892 let mut serials = Vec::with_capacity(array.data.len());
893 let mut has_time = false;
894 for text in &array.data {
895 let (naive, parsed_has_time) =
896 parse_datetime_text_with_input_format(text, input_format).ok_or_else(|| {
897 datetime_error(format!("datetime: unable to parse date/time text '{text}'"))
898 })?;
899 serials.push(datenum_from_naive(naive));
900 has_time |= parsed_has_time;
901 }
902 Ok((
903 serials,
904 tensor::default_shape_for(&array.shape, array.data.len()),
905 if has_time {
906 DEFAULT_DATETIME_FORMAT.to_string()
907 } else {
908 DEFAULT_DATE_FORMAT.to_string()
909 },
910 ))
911 }
912 Value::CharArray(array) => {
913 let mut texts = Vec::with_capacity(array.rows);
914 for row in 0..array.rows {
915 let start = row * array.cols;
916 let end = start + array.cols;
917 texts.push(
918 array.data[start..end]
919 .iter()
920 .collect::<String>()
921 .trim_end()
922 .to_string(),
923 );
924 }
925 parse_text_input(
926 Value::StringArray(
927 StringArray::new(texts, vec![array.rows, 1])
928 .map_err(|err| datetime_error(format!("datetime: {err}")))?,
929 ),
930 input_format,
931 )
932 }
933 _ => Err(datetime_error(
934 "datetime: text input must be a string scalar, string array, or character array",
935 )),
936 }
937}
938
939fn round_component(value: f64, label: &str, min: i64, max: i64) -> BuiltinResult<i64> {
940 if !value.is_finite() {
941 return Err(datetime_error(format!(
942 "datetime: {label} values must be finite"
943 )));
944 }
945 let rounded = value.round();
946 if (rounded - value).abs() > 1e-9 {
947 return Err(datetime_error(format!(
948 "datetime: {label} values must be integers"
949 )));
950 }
951 let integer = rounded as i64;
952 if integer < min || integer > max {
953 return Err(datetime_error(format!(
954 "datetime: {label} values must be in the range [{min}, {max}]"
955 )));
956 }
957 Ok(integer)
958}
959
960fn naive_from_components(
961 year: f64,
962 month: f64,
963 day: f64,
964 hour: f64,
965 minute: f64,
966 second: f64,
967) -> BuiltinResult<NaiveDateTime> {
968 let year = round_component(year, "year", -262_000, 262_000)? as i32;
969 let month = round_component(month, "month", 1, 12)? as u32;
970 let day = round_component(day, "day", 1, 31)? as u32;
971 let hour = round_component(hour, "hour", 0, 23)? as u32;
972 let minute = round_component(minute, "minute", 0, 59)? as u32;
973 if !second.is_finite() {
974 return Err(datetime_error("datetime: second values must be finite"));
975 }
976 if !(0.0..60.0).contains(&second) {
977 return Err(datetime_error(
978 "datetime: second values must be in the range [0, 60)",
979 ));
980 }
981
982 let base_date = NaiveDate::from_ymd_opt(year, month, day)
983 .ok_or_else(|| datetime_error("datetime: invalid calendar date"))?;
984 let whole_second = second.floor();
985 let mut nanos = ((second - whole_second) * 1_000_000_000.0).round() as u32;
986 let mut secs = whole_second as u32;
987 if nanos == 1_000_000_000 {
988 secs += 1;
989 nanos = 0;
990 }
991 let time = base_date
992 .and_hms_nano_opt(hour, minute, secs, nanos)
993 .ok_or_else(|| datetime_error("datetime: invalid time components"))?;
994 Ok(time)
995}
996
997fn broadcast_component_data(
998 arrays: &[Tensor],
999 labels: &[&str],
1000) -> BuiltinResult<(Vec<Vec<f64>>, Vec<usize>)> {
1001 let mut target_shape = vec![1, 1];
1002 let mut target_len = 1usize;
1003
1004 for array in arrays {
1005 let len = array.data.len();
1006 if len > 1 {
1007 let shape = tensor::default_shape_for(&array.shape, len);
1008 if target_len == 1 {
1009 target_len = len;
1010 target_shape = shape;
1011 } else if len != target_len || shape != target_shape {
1012 return Err(datetime_error(
1013 "datetime: non-scalar component inputs must have matching sizes",
1014 ));
1015 }
1016 }
1017 }
1018
1019 let mut broadcasted = Vec::with_capacity(arrays.len());
1020 for (idx, array) in arrays.iter().enumerate() {
1021 if array.data.len() == 1 {
1022 broadcasted.push(vec![array.data[0]; target_len]);
1023 } else if array.data.len() == target_len {
1024 broadcasted.push(array.data.clone());
1025 } else {
1026 return Err(datetime_error(format!(
1027 "datetime: {} input size does not match the other components",
1028 labels[idx]
1029 )));
1030 }
1031 }
1032
1033 Ok((broadcasted, target_shape))
1034}
1035
1036fn component_tensor(value: Value, context: &str) -> BuiltinResult<Tensor> {
1037 let tensor = tensor_from_numeric(value, context)?;
1038 Tensor::new(
1039 tensor.data.clone(),
1040 tensor::default_shape_for(&tensor.shape, tensor.data.len()),
1041 )
1042 .map_err(|err| datetime_error(format!("datetime: {err}")))
1043}
1044
1045fn build_from_components(args: Vec<Value>, format: Option<String>) -> BuiltinResult<Value> {
1046 let labels = ["year", "month", "day", "hour", "minute", "second"];
1047 let input_count = args.len();
1048 let mut arrays = Vec::with_capacity(args.len());
1049 for (idx, arg) in args.into_iter().enumerate() {
1050 arrays.push(component_tensor(arg, labels[idx])?);
1051 }
1052 while arrays.len() < 6 {
1053 arrays.push(Tensor::new(vec![0.0], vec![1, 1]).unwrap());
1054 }
1055
1056 let (broadcasted, shape) = broadcast_component_data(&arrays, &labels)?;
1057 let len = broadcasted[0].len();
1058 let mut serials = Vec::with_capacity(len);
1059 for idx in 0..len {
1060 let naive = naive_from_components(
1061 broadcasted[0][idx],
1062 broadcasted[1][idx],
1063 broadcasted[2][idx],
1064 broadcasted[3][idx],
1065 broadcasted[4][idx],
1066 broadcasted[5][idx],
1067 )?;
1068 serials.push(datenum_from_naive(naive));
1069 }
1070
1071 let default_format = if let Some(format) = format {
1072 format
1073 } else if input_count > 3 {
1074 DEFAULT_DATETIME_FORMAT.to_string()
1075 } else {
1076 DEFAULT_DATE_FORMAT.to_string()
1077 };
1078 datetime_object_from_serials(serials, shape, default_format)
1079}
1080
1081fn numeric_value_to_datetime(value: Value, format: Option<String>) -> BuiltinResult<Value> {
1082 let serials = serial_tensor_from_value(value, "datetime")?;
1083 datetime_object_from_serial_tensor(
1084 serials,
1085 format.unwrap_or_else(|| DEFAULT_DATETIME_FORMAT.to_string()),
1086 )
1087}
1088
1089pub fn is_datetime_object(value: &Value) -> bool {
1090 matches!(value, Value::Object(obj) if obj.is_class(DATETIME_CLASS))
1091}
1092
1093pub fn is_calendar_duration_object(value: &Value) -> bool {
1094 matches!(value, Value::Object(obj) if obj.is_class(CALENDAR_DURATION_CLASS))
1095}
1096
1097fn calendar_duration_tensor_for_object(obj: &ObjectInstance, field: &str) -> BuiltinResult<Tensor> {
1098 match obj.properties.get(field) {
1099 Some(Value::Tensor(tensor)) => Ok(tensor.clone()),
1100 Some(Value::Num(value)) => Tensor::new(vec![*value], vec![1, 1])
1101 .map_err(|err| datetime_error(format!("calendarDuration: {err}"))),
1102 Some(other) => Err(datetime_error(format!(
1103 "calendarDuration: invalid internal {field} storage {other:?}"
1104 ))),
1105 None => Err(datetime_error(format!(
1106 "calendarDuration: missing internal {field} storage"
1107 ))),
1108 }
1109}
1110
1111fn calendar_duration_tensors_from_value(value: &Value) -> BuiltinResult<(Tensor, Tensor)> {
1112 match value {
1113 Value::Object(obj) if obj.is_class(CALENDAR_DURATION_CLASS) => Ok((
1114 calendar_duration_tensor_for_object(obj, CALENDAR_MONTHS_FIELD)?,
1115 calendar_duration_tensor_for_object(obj, CALENDAR_DAYS_FIELD)?,
1116 )),
1117 _ => Err(datetime_error(
1118 "calendarDuration: expected a calendarDuration value",
1119 )),
1120 }
1121}
1122
1123fn calendar_duration_object_from_tensors(months: Tensor, days: Tensor) -> BuiltinResult<Value> {
1124 ensure_calendar_duration_class_registered();
1125 let mut object = ObjectInstance::new(CALENDAR_DURATION_CLASS.to_string());
1126 object
1127 .properties
1128 .insert(CALENDAR_MONTHS_FIELD.to_string(), Value::Tensor(months));
1129 object
1130 .properties
1131 .insert(CALENDAR_DAYS_FIELD.to_string(), Value::Tensor(days));
1132 Ok(Value::Object(object))
1133}
1134
1135fn calendar_duration_object_from_components(
1136 months: Vec<f64>,
1137 days: Vec<f64>,
1138 shape: Vec<usize>,
1139) -> BuiltinResult<Value> {
1140 let month_tensor = Tensor::new(months, shape.clone())
1141 .map_err(|err| datetime_error(format!("calendarDuration: {err}")))?;
1142 let day_tensor = Tensor::new(days, shape)
1143 .map_err(|err| datetime_error(format!("calendarDuration: {err}")))?;
1144 calendar_duration_object_from_tensors(month_tensor, day_tensor)
1145}
1146
1147fn calendar_duration_unit_value(
1148 value: Value,
1149 unit_name: &str,
1150 months_per_unit: f64,
1151 days_per_unit: f64,
1152) -> BuiltinResult<Value> {
1153 if is_calendar_duration_object(&value) {
1154 let (months, days) = calendar_duration_tensors_from_value(&value)?;
1155 let (month_data, day_data, shape) =
1156 tensor::binary_numeric_tensors(&months, &days, unit_name, BUILTIN_NAME)?;
1157 let data = month_data
1158 .iter()
1159 .zip(day_data.iter())
1160 .map(|(months, days)| {
1161 if months_per_unit != 0.0 {
1162 months / months_per_unit + days / 30.436875 / months_per_unit
1163 } else {
1164 days / days_per_unit
1165 }
1166 })
1167 .collect::<Vec<_>>();
1168 return tensor_or_scalar(data, shape);
1169 }
1170
1171 let numeric = component_tensor(value, unit_name)?;
1172 let shape = tensor::default_shape_for(&numeric.shape, numeric.data.len());
1173 let mut months = Vec::with_capacity(numeric.data.len());
1174 let mut days = Vec::with_capacity(numeric.data.len());
1175 for value in &numeric.data {
1176 if !value.is_finite() {
1177 return Err(datetime_error(format!(
1178 "{unit_name}: values must be finite"
1179 )));
1180 }
1181 let month_value = value * months_per_unit;
1182 let day_value = value * days_per_unit;
1183 if !month_value.is_finite() || !day_value.is_finite() {
1184 return Err(datetime_error(format!(
1185 "{unit_name}: resulting calendar duration is outside supported range"
1186 )));
1187 }
1188 months.push(month_value);
1189 days.push(day_value);
1190 }
1191 calendar_duration_object_from_components(months, days, shape)
1192}
1193
1194fn add_months_clamped(value: NaiveDateTime, month_delta: i64) -> BuiltinResult<NaiveDateTime> {
1195 let current_month = i64::from(value.year())
1196 .checked_mul(12)
1197 .and_then(|base| base.checked_add(i64::from(value.month() - 1)))
1198 .ok_or_else(|| datetime_error("calendarDuration: result date is out of range"))?;
1199 let zero_based = current_month
1200 .checked_add(month_delta)
1201 .ok_or_else(|| datetime_error("calendarDuration: result date is out of range"))?;
1202 let year_i64 = zero_based.div_euclid(12);
1203 let year = i32::try_from(year_i64)
1204 .map_err(|_| datetime_error("calendarDuration: result date is out of range"))?;
1205 let month = zero_based.rem_euclid(12) as u32 + 1;
1206 let day = value.day().min(days_in_month(year, month)?);
1207 NaiveDate::from_ymd_opt(year, month, day)
1208 .and_then(|date| {
1209 date.and_hms_nano_opt(
1210 value.hour(),
1211 value.minute(),
1212 value.second(),
1213 value.nanosecond(),
1214 )
1215 })
1216 .ok_or_else(|| datetime_error("calendarDuration: result date is out of range"))
1217}
1218
1219fn add_fractional_days(value: NaiveDateTime, days: f64) -> BuiltinResult<NaiveDateTime> {
1220 if !days.is_finite() {
1221 return Err(datetime_error(
1222 "calendarDuration: day components must be finite",
1223 ));
1224 }
1225 let nanos = (days * SECONDS_PER_DAY * 1_000_000_000.0).round();
1226 if !nanos.is_finite() || nanos < i64::MIN as f64 || nanos > i64::MAX as f64 {
1227 return Err(datetime_error(
1228 "calendarDuration: day component is outside supported range",
1229 ));
1230 }
1231 Ok(value + Duration::nanoseconds(nanos as i64))
1232}
1233
1234fn apply_calendar_duration_to_serials(
1235 serials: &Tensor,
1236 months: &Tensor,
1237 days: &Tensor,
1238 sign: f64,
1239 context: &str,
1240) -> BuiltinResult<(Vec<f64>, Vec<usize>)> {
1241 let (serial_data, month_data, day_data, shape) =
1242 broadcast_three_numeric_tensors(serials, months, days, context)?;
1243 let mut out = Vec::with_capacity(serial_data.len());
1244 for ((serial, months), days) in serial_data
1245 .iter()
1246 .zip(month_data.iter())
1247 .zip(day_data.iter())
1248 {
1249 if !months.is_finite() {
1250 return Err(datetime_error(format!(
1251 "{context}: month components must be finite"
1252 )));
1253 }
1254 let signed_months = months * sign;
1255 let rounded_months = signed_months.round();
1256 if (rounded_months - signed_months).abs() > 1e-9 {
1257 return Err(datetime_error(format!(
1258 "{context}: calendar month components must be integers for datetime arithmetic"
1259 )));
1260 }
1261 if rounded_months < i64::MIN as f64 || rounded_months > i64::MAX as f64 {
1262 return Err(datetime_error(format!(
1263 "{context}: calendar month component is outside supported range"
1264 )));
1265 }
1266 let shifted = add_months_clamped(naive_from_datenum(*serial)?, rounded_months as i64)?;
1267 out.push(datenum_from_naive(add_fractional_days(
1268 shifted,
1269 days * sign,
1270 )?));
1271 }
1272 Ok((out, shape))
1273}
1274
1275pub(crate) fn serials_from_datetime_value(value: &Value) -> BuiltinResult<Tensor> {
1276 match value {
1277 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => serial_tensor_for_object(obj),
1278 _ => Err(datetime_error("datetime: expected a datetime value")),
1279 }
1280}
1281
1282pub(crate) fn datetime_format_from_value(value: &Value) -> String {
1283 match value {
1284 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => format_for_object(obj),
1285 _ => DEFAULT_DATETIME_FORMAT.to_string(),
1286 }
1287}
1288
1289pub fn datetime_string_array(value: &Value) -> BuiltinResult<Option<StringArray>> {
1290 let Value::Object(obj) = value else {
1291 return Ok(None);
1292 };
1293 if !obj.is_class(DATETIME_CLASS) {
1294 return Ok(None);
1295 }
1296 let serials = serial_tensor_for_object(obj)?;
1297 let format = format_for_object(obj);
1298 let mut strings = Vec::with_capacity(serials.data.len());
1299 for serial in &serials.data {
1300 strings.push(format_serial(*serial, &format)?);
1301 }
1302 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
1303 let array = StringArray::new(strings, shape)
1304 .map_err(|err| datetime_error(format!("datetime: {err}")))?;
1305 Ok(Some(array))
1306}
1307
1308pub fn datetime_display_text(value: &Value) -> BuiltinResult<Option<String>> {
1309 let Some(array) = datetime_string_array(value)? else {
1310 return Ok(None);
1311 };
1312 if array.data.len() == 1 {
1313 return Ok(Some(array.data[0].clone()));
1314 }
1315
1316 let rows = array.rows;
1317 let cols = array.cols;
1318 let mut widths = vec![0usize; cols];
1319 for col in 0..cols {
1320 for row in 0..rows {
1321 let idx = row + col * rows;
1322 widths[col] = widths[col].max(array.data[idx].chars().count());
1323 }
1324 }
1325
1326 let mut lines = Vec::with_capacity(rows);
1327 for row in 0..rows {
1328 let mut line = String::new();
1329 for col in 0..cols {
1330 if col > 0 {
1331 line.push_str(" ");
1332 }
1333 let idx = row + col * rows;
1334 let text = &array.data[idx];
1335 line.push_str(text);
1336 let padding = widths[col].saturating_sub(text.chars().count());
1337 if padding > 0 {
1338 line.push_str(&" ".repeat(padding));
1339 }
1340 }
1341 lines.push(line);
1342 }
1343 Ok(Some(lines.join("\n")))
1344}
1345
1346pub fn datetime_summary(value: &Value) -> BuiltinResult<Option<String>> {
1347 let Value::Object(obj) = value else {
1348 return Ok(None);
1349 };
1350 if !obj.is_class(DATETIME_CLASS) {
1351 return Ok(None);
1352 }
1353 let serials = serial_tensor_for_object(obj)?;
1354 if serials.data.len() == 1 {
1355 return datetime_display_text(value);
1356 }
1357 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
1358 Ok(Some(format!(
1359 "[{} datetime]",
1360 shape
1361 .iter()
1362 .map(|dim| dim.to_string())
1363 .collect::<Vec<_>>()
1364 .join("x")
1365 )))
1366}
1367
1368fn component_tensor_from_datetime(
1369 value: &Value,
1370 label: &str,
1371 extractor: impl Fn(&NaiveDateTime) -> f64,
1372) -> BuiltinResult<Value> {
1373 let serials = serials_from_datetime_value(value)?;
1374 let mut out = Vec::with_capacity(serials.data.len());
1375 for serial in &serials.data {
1376 let naive = naive_from_datenum(*serial)?;
1377 out.push(extractor(&naive));
1378 }
1379 if out.len() == 1 {
1380 Ok(Value::Num(out[0]))
1381 } else {
1382 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
1383 let tensor =
1384 Tensor::new(out, shape).map_err(|err| datetime_error(format!("{label}: {err}")))?;
1385 Ok(Value::Tensor(tensor))
1386 }
1387}
1388
1389fn tensor_or_scalar(data: Vec<f64>, shape: Vec<usize>) -> BuiltinResult<Value> {
1390 if data.len() == 1 {
1391 Ok(Value::Num(data[0]))
1392 } else {
1393 Ok(Value::Tensor(Tensor::new(data, shape).map_err(|err| {
1394 datetime_error(format!("datetime: {err}"))
1395 })?))
1396 }
1397}
1398
1399fn numeric_or_datetime_serial_tensor(value: Value, context: &str) -> BuiltinResult<Tensor> {
1400 match &value {
1401 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => serial_tensor_for_object(obj),
1402 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => {
1403 let (serials, shape, _) = parse_text_input(value, None)?;
1404 Tensor::new(serials, shape).map_err(|err| datetime_error(format!("{context}: {err}")))
1405 }
1406 _ => serial_tensor_from_value(value, context),
1407 }
1408}
1409
1410fn datevec_components_from_serial(serial: f64) -> BuiltinResult<[f64; 6]> {
1411 let naive = naive_from_datenum(serial)?;
1412 Ok([
1413 naive.year() as f64,
1414 naive.month() as f64,
1415 naive.day() as f64,
1416 naive.hour() as f64,
1417 naive.minute() as f64,
1418 naive.second() as f64 + f64::from(naive.nanosecond()) / 1_000_000_000.0,
1419 ])
1420}
1421
1422fn datevec_matrix_from_serial_tensor(serials: &Tensor) -> BuiltinResult<Tensor> {
1423 let rows = serials.data.len();
1424 let mut data = vec![0.0; rows.saturating_mul(6)];
1425 for (row, serial) in serials.data.iter().enumerate() {
1426 let components = datevec_components_from_serial(*serial)?;
1427 for col in 0..6 {
1428 data[col * rows + row] = components[col];
1429 }
1430 }
1431 Tensor::new(data, vec![rows, 6]).map_err(|err| datetime_error(format!("datevec: {err}")))
1432}
1433
1434fn datetime_from_date_only(
1435 naive: NaiveDateTime,
1436 format: impl Into<String>,
1437) -> BuiltinResult<Value> {
1438 datetime_object_from_serials(vec![datenum_from_naive(naive)], vec![1, 1], format)
1439}
1440
1441fn current_naive_local() -> NaiveDateTime {
1442 Local::now().naive_local()
1443}
1444
1445fn days_in_month(year: i32, month: u32) -> BuiltinResult<u32> {
1446 let _ = NaiveDate::from_ymd_opt(year, month, 1)
1447 .ok_or_else(|| datetime_error("eomday: invalid year/month"))?;
1448 let (next_year, next_month) = if month == 12 {
1449 (year + 1, 1)
1450 } else {
1451 (year, month + 1)
1452 };
1453 let next = NaiveDate::from_ymd_opt(next_year, next_month, 1)
1454 .ok_or_else(|| datetime_error("eomday: invalid year/month"))?;
1455 Ok((next - Duration::days(1)).day())
1456}
1457
1458fn tensor_from_datevec_like(value: Value, context: &str) -> BuiltinResult<Tensor> {
1459 let tensor = tensor_from_numeric(value, context)?;
1460 let shape = tensor::default_shape_for(&tensor.shape, tensor.data.len());
1461 let normalize = |rows: usize, cols: usize, data: Vec<f64>| -> BuiltinResult<Tensor> {
1462 if cols == 6 {
1463 return Tensor::new(data, vec![rows, 6])
1464 .map_err(|err| datetime_error(format!("{context}: {err}")));
1465 }
1466 let mut padded = vec![0.0; rows.saturating_mul(6)];
1467 for col in 0..3 {
1468 for row in 0..rows {
1469 padded[col * rows + row] = data[col * rows + row];
1470 }
1471 }
1472 Tensor::new(padded, vec![rows, 6])
1473 .map_err(|err| datetime_error(format!("{context}: {err}")))
1474 };
1475 if tensor.data.len() == 3 {
1476 return normalize(1, 3, tensor.data);
1477 }
1478 if tensor.data.len() == 6 {
1479 return normalize(1, 6, tensor.data);
1480 }
1481 if shape.len() >= 2 && (shape[1] == 3 || shape[1] == 6) {
1482 return normalize(shape[0], shape[1], tensor.data);
1483 }
1484 Err(datetime_error(format!(
1485 "{context}: expected a date vector with three or six columns"
1486 )))
1487}
1488
1489fn datenum_from_datevec_tensor(tensor: &Tensor, context: &str) -> BuiltinResult<Tensor> {
1490 let rows = tensor.rows;
1491 let cols = tensor.cols;
1492 if cols != 6 {
1493 return Err(datetime_error(format!(
1494 "{context}: date vectors must have six columns"
1495 )));
1496 }
1497 let mut out = Vec::with_capacity(rows);
1498 for row in 0..rows {
1499 let component = |col: usize| tensor.data[col * rows + row];
1500 let naive = naive_from_components(
1501 component(0),
1502 component(1),
1503 component(2),
1504 component(3),
1505 component(4),
1506 component(5),
1507 )?;
1508 out.push(datenum_from_naive(naive));
1509 }
1510 Tensor::new(out, vec![rows, 1]).map_err(|err| datetime_error(format!("{context}: {err}")))
1511}
1512
1513fn char_array_from_rows(rows: &[String], context: &str) -> BuiltinResult<CharArray> {
1514 let width = rows
1515 .iter()
1516 .map(|row| row.chars().count())
1517 .max()
1518 .unwrap_or(0);
1519 let mut data = vec![' '; rows.len().saturating_mul(width)];
1520 for (row_idx, row) in rows.iter().enumerate() {
1521 for (col, ch) in row.chars().enumerate() {
1522 data[row_idx * width + col] = ch;
1523 }
1524 }
1525 CharArray::new(data, rows.len(), width)
1526 .map_err(|err| datetime_error(format!("{context}: {err}")))
1527}
1528
1529fn broadcast_three_numeric_tensors(
1530 a: &Tensor,
1531 b: &Tensor,
1532 c: &Tensor,
1533 context: &str,
1534) -> BuiltinResult<Broadcast3> {
1535 let mut output_shape = Vec::new();
1536 let mut output_len = 1usize;
1537 for operand in [a, b, c] {
1538 if operand.data.len() == 1 {
1539 continue;
1540 }
1541 let shape = tensor::default_shape_for(&operand.shape, operand.data.len());
1542 if output_shape.is_empty() {
1543 output_len = operand.data.len();
1544 output_shape = shape;
1545 } else if operand.data.len() != output_len || shape != output_shape {
1546 return Err(datetime_error(format!(
1547 "{context}: operands must be scalar or have matching sizes"
1548 )));
1549 }
1550 }
1551 if output_shape.is_empty() {
1552 output_shape = vec![1, 1];
1553 output_len = 1;
1554 }
1555
1556 let expand = |operand: &Tensor| -> BuiltinResult<Vec<f64>> {
1557 match operand.data.len() {
1558 1 => Ok(vec![operand.data[0]; output_len]),
1559 len if len == output_len => Ok(operand.data.clone()),
1560 _ => Err(datetime_error(format!(
1561 "{context}: operands must be scalar or have matching sizes"
1562 ))),
1563 }
1564 };
1565
1566 Ok((expand(a)?, expand(b)?, expand(c)?, output_shape))
1567}
1568
1569fn serial_date_key(serial: f64) -> BuiltinResult<i64> {
1570 if !serial.is_finite() {
1571 return Err(datetime_error("date values must be finite"));
1572 }
1573 let key = serial.floor();
1574 if key < i64::MIN as f64 || key > i64::MAX as f64 {
1575 return Err(datetime_error("date value is outside supported range"));
1576 }
1577 Ok(key as i64)
1578}
1579
1580fn date_from_key(key: i64) -> BuiltinResult<NaiveDate> {
1581 Ok(naive_from_datenum(key as f64)?.date())
1582}
1583
1584fn key_from_date(date: NaiveDate) -> i64 {
1585 datenum_from_naive(midnight(date)).floor() as i64
1586}
1587
1588fn observed_fixed_holiday(year: i32, month: u32, day: u32) -> BuiltinResult<i64> {
1589 let date = NaiveDate::from_ymd_opt(year, month, day)
1590 .ok_or_else(|| datetime_error("holidays: invalid fixed holiday date"))?;
1591 let observed = match date.weekday() {
1592 Weekday::Sat => date - Duration::days(1),
1593 Weekday::Sun => date + Duration::days(1),
1594 _ => date,
1595 };
1596 Ok(key_from_date(observed))
1597}
1598
1599fn nth_weekday(year: i32, month: u32, weekday: Weekday, n: u32) -> BuiltinResult<i64> {
1600 let mut date = NaiveDate::from_ymd_opt(year, month, 1)
1601 .ok_or_else(|| datetime_error("holidays: invalid nth weekday month"))?;
1602 while date.weekday() != weekday {
1603 date += Duration::days(1);
1604 }
1605 date += Duration::days(i64::from(n.saturating_sub(1)) * 7);
1606 Ok(key_from_date(date))
1607}
1608
1609fn last_weekday(year: i32, month: u32, weekday: Weekday) -> BuiltinResult<i64> {
1610 let last_day = days_in_month(year, month)?;
1611 let mut date = NaiveDate::from_ymd_opt(year, month, last_day)
1612 .ok_or_else(|| datetime_error("holidays: invalid last weekday month"))?;
1613 while date.weekday() != weekday {
1614 date -= Duration::days(1);
1615 }
1616 Ok(key_from_date(date))
1617}
1618
1619fn easter_sunday(year: i32) -> BuiltinResult<NaiveDate> {
1620 let a = year.rem_euclid(19);
1621 let b = year.div_euclid(100);
1622 let c = year.rem_euclid(100);
1623 let d = b.div_euclid(4);
1624 let e = b.rem_euclid(4);
1625 let f = (b + 8).div_euclid(25);
1626 let g = (b - f + 1).div_euclid(3);
1627 let h = (19 * a + b - d - g + 15).rem_euclid(30);
1628 let i = c.div_euclid(4);
1629 let k = c.rem_euclid(4);
1630 let l = (32 + 2 * e + 2 * i - h - k).rem_euclid(7);
1631 let m = (a + 11 * h + 22 * l).div_euclid(451);
1632 let month = (h + l - 7 * m + 114).div_euclid(31) as u32;
1633 let day = ((h + l - 7 * m + 114).rem_euclid(31) + 1) as u32;
1634 NaiveDate::from_ymd_opt(year, month, day)
1635 .ok_or_else(|| datetime_error("holidays: invalid computed Easter date"))
1636}
1637
1638fn market_holiday_keys_for_year(year: i32) -> BuiltinResult<Vec<i64>> {
1639 let mut keys = vec![
1640 observed_fixed_holiday(year, 1, 1)?,
1641 nth_weekday(year, 1, Weekday::Mon, 3)?,
1642 nth_weekday(year, 2, Weekday::Mon, 3)?,
1643 key_from_date(easter_sunday(year)? - Duration::days(2)),
1644 last_weekday(year, 5, Weekday::Mon)?,
1645 observed_fixed_holiday(year, 6, 19)?,
1646 observed_fixed_holiday(year, 7, 4)?,
1647 nth_weekday(year, 9, Weekday::Mon, 1)?,
1648 nth_weekday(year, 11, Weekday::Thu, 4)?,
1649 observed_fixed_holiday(year, 12, 25)?,
1650 ];
1651 keys.sort_unstable();
1652 keys.dedup();
1653 Ok(keys)
1654}
1655
1656fn holiday_keys_between(start_key: i64, end_key: i64) -> BuiltinResult<Vec<i64>> {
1657 let start_year = date_from_key(start_key.min(end_key))?
1658 .year()
1659 .checked_sub(1)
1660 .ok_or_else(|| datetime_error("holidays: date range is outside supported range"))?;
1661 let end_year = date_from_key(start_key.max(end_key))?
1662 .year()
1663 .checked_add(1)
1664 .ok_or_else(|| datetime_error("holidays: date range is outside supported range"))?;
1665 if end_year - start_year > MAX_HOLIDAY_YEAR_SPAN {
1666 return Err(datetime_error(format!(
1667 "holidays: date range spans more than {MAX_HOLIDAY_YEAR_SPAN} years"
1668 )));
1669 }
1670 let mut keys = Vec::new();
1671 for year in start_year..=end_year {
1672 keys.extend(market_holiday_keys_for_year(year)?);
1673 }
1674 keys.sort_unstable();
1675 keys.dedup();
1676 Ok(keys
1677 .into_iter()
1678 .filter(|key| *key >= start_key.min(end_key) && *key <= start_key.max(end_key))
1679 .collect())
1680}
1681
1682fn holiday_set_for_range(start_key: i64, end_key: i64) -> BuiltinResult<HashSet<i64>> {
1683 Ok(holiday_keys_between(start_key, end_key)?
1684 .into_iter()
1685 .collect())
1686}
1687
1688fn holiday_set_from_optional_or_default(
1689 value: Option<Value>,
1690 context: &str,
1691 start_key: i64,
1692 end_key: i64,
1693) -> BuiltinResult<HashSet<i64>> {
1694 if let Some(value) = value {
1695 let serials = numeric_or_datetime_serial_tensor(value, context)?;
1696 return serials
1697 .data
1698 .iter()
1699 .map(|serial| serial_date_key(*serial))
1700 .collect::<BuiltinResult<HashSet<_>>>();
1701 }
1702 holiday_set_for_range(start_key, end_key)
1703}
1704
1705fn date_key_range(tensors: &[&Tensor]) -> BuiltinResult<(i64, i64)> {
1706 let mut min_key = i64::MAX;
1707 let mut max_key = i64::MIN;
1708 let mut found = false;
1709 for tensor in tensors {
1710 for serial in &tensor.data {
1711 let key = serial_date_key(*serial)?;
1712 min_key = min_key.min(key);
1713 max_key = max_key.max(key);
1714 found = true;
1715 }
1716 }
1717 if found {
1718 Ok((min_key, max_key))
1719 } else {
1720 Ok((0, 0))
1721 }
1722}
1723
1724fn is_business_day_key(key: i64, holidays: &HashSet<i64>) -> BuiltinResult<bool> {
1725 let date = date_from_key(key)?;
1726 Ok(!matches!(date.weekday(), Weekday::Sat | Weekday::Sun) && !holidays.contains(&key))
1727}
1728
1729fn count_weekdays_forward(start_key: i64, end_key: i64) -> BuiltinResult<i64> {
1730 let total_days = end_key
1731 .checked_sub(start_key)
1732 .and_then(|delta| delta.checked_add(1))
1733 .ok_or_else(|| datetime_error("business-day date range is outside supported range"))?;
1734 let full_weeks = total_days / 7;
1735 let mut count = full_weeks * 5;
1736 let remainder = total_days % 7;
1737 for offset in 0..remainder {
1738 let key = start_key
1739 .checked_add(offset)
1740 .ok_or_else(|| datetime_error("business-day date range is outside supported range"))?;
1741 if !matches!(date_from_key(key)?.weekday(), Weekday::Sat | Weekday::Sun) {
1742 count += 1;
1743 }
1744 }
1745 Ok(count)
1746}
1747
1748fn count_business_days(
1749 start_key: i64,
1750 end_key: i64,
1751 holidays: &HashSet<i64>,
1752) -> BuiltinResult<i64> {
1753 if start_key > end_key {
1754 return Ok(-count_business_days(end_key, start_key, holidays)?);
1755 }
1756 let mut count = count_weekdays_forward(start_key, end_key)?;
1757 for holiday in holidays {
1758 if *holiday >= start_key
1759 && *holiday <= end_key
1760 && !matches!(
1761 date_from_key(*holiday)?.weekday(),
1762 Weekday::Sat | Weekday::Sun
1763 )
1764 {
1765 count -= 1;
1766 }
1767 }
1768 Ok(count)
1769}
1770
1771fn first_business_day_key(year: i32, month: u32, holidays: &HashSet<i64>) -> BuiltinResult<i64> {
1772 let mut date = NaiveDate::from_ymd_opt(year, month, 1)
1773 .ok_or_else(|| datetime_error("fbusdate: invalid year/month"))?;
1774 loop {
1775 let key = key_from_date(date);
1776 if is_business_day_key(key, holidays)? {
1777 return Ok(key);
1778 }
1779 date += Duration::days(1);
1780 }
1781}
1782
1783fn last_business_day_key(year: i32, month: u32, holidays: &HashSet<i64>) -> BuiltinResult<i64> {
1784 let mut date = NaiveDate::from_ymd_opt(year, month, days_in_month(year, month)?)
1785 .ok_or_else(|| datetime_error("lbusdate: invalid year/month"))?;
1786 loop {
1787 let key = key_from_date(date);
1788 if is_business_day_key(key, holidays)? {
1789 return Ok(key);
1790 }
1791 date -= Duration::days(1);
1792 }
1793}
1794
1795async fn datetime_indexing(obj: Value, payload: Value) -> BuiltinResult<Value> {
1796 let Value::Object(object) = obj else {
1797 return Err(datetime_error(
1798 "datetime.subsref: receiver must be a datetime object",
1799 ));
1800 };
1801 let format = format_for_object(&object);
1802 let serials = serial_tensor_for_object(&object)?;
1803
1804 let Value::Cell(cell) = payload else {
1805 return Err(datetime_error(
1806 "datetime.subsref: indexing payload must be a cell array",
1807 ));
1808 };
1809 if cell.data.is_empty() {
1810 return datetime_object_from_serial_tensor(serials, format);
1811 }
1812 if cell.data.len() != 1 {
1813 return Err(datetime_error(
1814 "datetime.subsref: only linear datetime indexing is currently supported",
1815 ));
1816 }
1817 let selector = cell.data[0].clone();
1818 let selector = match selector {
1819 Value::Tensor(tensor) => tensor,
1820 Value::Num(value) => Tensor::new(vec![value], vec![1, 1])
1821 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1822 Value::Int(value) => Tensor::new(vec![value.to_f64()], vec![1, 1])
1823 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1824 Value::LogicalArray(logical) => tensor::logical_to_tensor(&logical)
1825 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1826 other => {
1827 return Err(datetime_error(format!(
1828 "datetime.subsref: unsupported index value {other:?}"
1829 )))
1830 }
1831 };
1832 let indexed = crate::perform_indexing(&Value::Tensor(serials), &selector.data)
1833 .await
1834 .map_err(|err| datetime_error(format!("datetime.subsref: {}", err.message())))?;
1835 let indexed_serials = match indexed {
1836 Value::Num(value) => Tensor::new(vec![value], vec![1, 1])
1837 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1838 Value::Tensor(tensor) => tensor,
1839 other => {
1840 return Err(datetime_error(format!(
1841 "datetime.subsref: unexpected indexing result {other:?}"
1842 )))
1843 }
1844 };
1845 datetime_object_from_serial_tensor(indexed_serials, format)
1846}
1847
1848#[runmat_macros::runtime_builtin(
1849 name = "datetime",
1850 descriptor(crate::builtins::datetime::DATETIME_DESCRIPTOR),
1851 builtin_path = "crate::builtins::datetime",
1852 category = "datetime",
1853 summary = "Create datetime arrays from text, components, or serial date numbers.",
1854 keywords = "datetime,date,time,datenum,Format",
1855 related = "year,month,day,hour,minute,second,string,char,disp",
1856 examples = "t = datetime(2024, 4, 9, 13, 30, 0);"
1857)]
1858async fn datetime_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
1859 ensure_datetime_class_registered();
1860 let args = gather_args(&args).await?;
1861 let (positional_end, options) = parse_trailing_options(&args)?;
1862 let positional = args[..positional_end].to_vec();
1863
1864 if let Some(convert_from) = options.convert_from {
1865 if !convert_from.eq_ignore_ascii_case("datenum") {
1866 return Err(datetime_error(format!(
1867 "datetime: unsupported ConvertFrom value '{convert_from}'"
1868 )));
1869 }
1870 if positional.len() != 1 {
1871 return Err(datetime_error(
1872 "datetime: ConvertFrom='datenum' expects exactly one numeric input",
1873 ));
1874 }
1875 return numeric_value_to_datetime(positional[0].clone(), options.format);
1876 }
1877
1878 match positional.len() {
1879 0 => {
1880 let now = Local::now().naive_local();
1881 datetime_object_from_serials(
1882 vec![datenum_from_naive(now)],
1883 vec![1, 1],
1884 options
1885 .format
1886 .unwrap_or_else(|| DEFAULT_DATETIME_FORMAT.to_string()),
1887 )
1888 }
1889 1 => match &positional[0] {
1890 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => {
1891 let serials = serials_from_datetime_value(&positional[0])?;
1892 let format = options
1893 .format
1894 .unwrap_or_else(|| datetime_format_from_value(&positional[0]));
1895 datetime_object_from_serial_tensor(serials, format)
1896 }
1897 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => {
1898 let (serials, shape, inferred_format) =
1899 parse_text_input(positional[0].clone(), options.input_format.as_deref())?;
1900 datetime_object_from_serials(
1901 serials,
1902 shape,
1903 options.format.unwrap_or(inferred_format),
1904 )
1905 }
1906 _ => numeric_value_to_datetime(positional[0].clone(), options.format),
1907 },
1908 3..=6 => build_from_components(positional, options.format),
1909 _ => Err(datetime_error(
1910 "datetime: unsupported argument pattern; use text, serial dates, or Y/M/D component inputs",
1911 )),
1912 }
1913}
1914
1915#[runmat_macros::runtime_builtin(
1916 name = "year",
1917 descriptor(crate::builtins::datetime::DATETIME_YEAR_DESCRIPTOR),
1918 builtin_path = "crate::builtins::datetime",
1919 category = "datetime",
1920 summary = "Extract calendar year components from datetime values.",
1921 keywords = "year,datetime,date component"
1922)]
1923async fn year_builtin(value: Value) -> crate::BuiltinResult<Value> {
1924 component_tensor_from_datetime(&value, "year", |naive| naive.year() as f64)
1925}
1926
1927#[runmat_macros::runtime_builtin(
1928 name = "month",
1929 descriptor(crate::builtins::datetime::DATETIME_MONTH_DESCRIPTOR),
1930 builtin_path = "crate::builtins::datetime",
1931 category = "datetime",
1932 summary = "Extract month numbers from datetime arrays.",
1933 keywords = "month,datetime,date component"
1934)]
1935async fn month_builtin(value: Value) -> crate::BuiltinResult<Value> {
1936 component_tensor_from_datetime(&value, "month", |naive| naive.month() as f64)
1937}
1938
1939#[runmat_macros::runtime_builtin(
1940 name = "day",
1941 descriptor(crate::builtins::datetime::DATETIME_DAY_DESCRIPTOR),
1942 builtin_path = "crate::builtins::datetime",
1943 category = "datetime",
1944 summary = "Extract day-of-month numbers from datetime values.",
1945 keywords = "day,datetime,date component"
1946)]
1947async fn day_builtin(value: Value) -> crate::BuiltinResult<Value> {
1948 component_tensor_from_datetime(&value, "day", |naive| naive.day() as f64)
1949}
1950
1951#[runmat_macros::runtime_builtin(
1952 name = "hour",
1953 descriptor(crate::builtins::datetime::DATETIME_HOUR_DESCRIPTOR),
1954 builtin_path = "crate::builtins::datetime",
1955 category = "datetime",
1956 summary = "Extract hour components from datetime values.",
1957 keywords = "hour,datetime,time component"
1958)]
1959async fn hour_builtin(value: Value) -> crate::BuiltinResult<Value> {
1960 component_tensor_from_datetime(&value, "hour", |naive| naive.hour() as f64)
1961}
1962
1963#[runmat_macros::runtime_builtin(
1964 name = "minute",
1965 descriptor(crate::builtins::datetime::DATETIME_MINUTE_DESCRIPTOR),
1966 builtin_path = "crate::builtins::datetime",
1967 category = "datetime",
1968 summary = "Extract minute numbers from datetime arrays.",
1969 keywords = "minute,datetime,time component"
1970)]
1971async fn minute_builtin(value: Value) -> crate::BuiltinResult<Value> {
1972 component_tensor_from_datetime(&value, "minute", |naive| naive.minute() as f64)
1973}
1974
1975#[runmat_macros::runtime_builtin(
1976 name = "second",
1977 descriptor(crate::builtins::datetime::DATETIME_SECOND_DESCRIPTOR),
1978 builtin_path = "crate::builtins::datetime",
1979 category = "datetime",
1980 summary = "Extract second components from datetime values.",
1981 keywords = "second,datetime,time component"
1982)]
1983async fn second_builtin(value: Value) -> crate::BuiltinResult<Value> {
1984 component_tensor_from_datetime(&value, "second", |naive| {
1985 naive.second() as f64 + f64::from(naive.nanosecond()) / 1_000_000_000.0
1986 })
1987}
1988
1989#[runmat_macros::runtime_builtin(
1990 name = "isdatetime",
1991 builtin_path = "crate::builtins::datetime",
1992 category = "datetime",
1993 summary = "Return true for datetime values.",
1994 keywords = "isdatetime,datetime,predicate"
1995)]
1996fn isdatetime_builtin(value: Value) -> crate::BuiltinResult<Value> {
1997 Ok(Value::Bool(is_datetime_object(&value)))
1998}
1999
2000#[runmat_macros::runtime_builtin(
2001 name = "now",
2002 builtin_path = "crate::builtins::datetime",
2003 category = "datetime",
2004 summary = "Return the current local date and time as a MATLAB serial date number.",
2005 keywords = "now,datenum,current time"
2006)]
2007fn now_builtin() -> crate::BuiltinResult<Value> {
2008 Ok(Value::Num(datenum_from_naive(current_naive_local())))
2009}
2010
2011#[runmat_macros::runtime_builtin(
2012 name = "today",
2013 builtin_path = "crate::builtins::datetime",
2014 category = "datetime",
2015 summary = "Return the current local date as a datetime scalar.",
2016 keywords = "today,datetime,current date"
2017)]
2018fn today_builtin() -> crate::BuiltinResult<Value> {
2019 let today = Local::now().date_naive().and_hms_opt(0, 0, 0).unwrap();
2020 datetime_from_date_only(today, DEFAULT_DATE_FORMAT)
2021}
2022
2023#[runmat_macros::runtime_builtin(
2024 name = "clock",
2025 builtin_path = "crate::builtins::datetime",
2026 category = "datetime",
2027 summary = "Return the current local date and time as a date vector.",
2028 keywords = "clock,datevec,current time"
2029)]
2030fn clock_builtin() -> crate::BuiltinResult<Value> {
2031 let components = datevec_components_from_serial(datenum_from_naive(current_naive_local()))?;
2032 Ok(Value::Tensor(
2033 Tensor::new(components.to_vec(), vec![1, 6])
2034 .map_err(|err| datetime_error(format!("clock: {err}")))?,
2035 ))
2036}
2037
2038#[runmat_macros::runtime_builtin(
2039 name = "datenum",
2040 builtin_path = "crate::builtins::datetime",
2041 category = "datetime",
2042 summary = "Convert date/time inputs to MATLAB serial date numbers.",
2043 keywords = "datenum,datetime,datevec,serial date"
2044)]
2045async fn datenum_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
2046 let args = gather_args(&args).await?;
2047 let tensor = match args.len() {
2048 0 => Tensor::new(vec![datenum_from_naive(current_naive_local())], vec![1, 1])
2049 .map_err(|err| datetime_error(format!("datenum: {err}")))?,
2050 1 => match &args[0] {
2051 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => serial_tensor_for_object(obj)?,
2052 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => {
2053 let (serials, shape, _) = parse_text_input(args[0].clone(), None)?;
2054 Tensor::new(serials, shape)
2055 .map_err(|err| datetime_error(format!("datenum: {err}")))?
2056 }
2057 Value::Tensor(_) => {
2058 if let Ok(datevec) = tensor_from_datevec_like(args[0].clone(), "datenum") {
2059 datenum_from_datevec_tensor(&datevec, "datenum")?
2060 } else {
2061 tensor_from_numeric(args[0].clone(), "datenum")?
2062 }
2063 }
2064 _ => tensor_from_numeric(args[0].clone(), "datenum")?,
2065 },
2066 3..=6 => {
2067 let datetime = build_from_components(args, None)?;
2068 serials_from_datetime_value(&datetime)?
2069 }
2070 _ => {
2071 return Err(datetime_error(
2072 "datenum: expected datetime, text, date vector, or Y/M/D components",
2073 ))
2074 }
2075 };
2076 if tensor.data.len() == 1 {
2077 Ok(Value::Num(tensor.data[0]))
2078 } else {
2079 Ok(Value::Tensor(tensor))
2080 }
2081}
2082
2083#[runmat_macros::runtime_builtin(
2084 name = "datevec",
2085 builtin_path = "crate::builtins::datetime",
2086 category = "datetime",
2087 summary = "Convert date/time inputs to date vectors.",
2088 keywords = "datevec,datetime,datenum"
2089)]
2090async fn datevec_builtin(value: Value) -> crate::BuiltinResult<Value> {
2091 let value = gather_if_needed_async(&value)
2092 .await
2093 .map_err(|err| datetime_error(format!("datevec: {}", err.message())))?;
2094 let serials = numeric_or_datetime_serial_tensor(value, "datevec")?;
2095 let matrix = datevec_matrix_from_serial_tensor(&serials)?;
2096 if let Some(out_count) = crate::output_count::current_output_count() {
2097 if out_count == 0 {
2098 return Ok(Value::OutputList(Vec::new()));
2099 }
2100 let mut outputs = Vec::with_capacity(out_count.min(6));
2101 for col in 0..6.min(out_count) {
2102 let mut data = Vec::with_capacity(matrix.rows);
2103 for row in 0..matrix.rows {
2104 data.push(matrix.data[col * matrix.rows + row]);
2105 }
2106 outputs.push(if data.len() == 1 {
2107 Value::Num(data[0])
2108 } else {
2109 Value::Tensor(
2110 Tensor::new(data, vec![matrix.rows, 1])
2111 .map_err(|err| datetime_error(format!("datevec: {err}")))?,
2112 )
2113 });
2114 }
2115 return Ok(crate::output_count::output_list_with_padding(
2116 out_count, outputs,
2117 ));
2118 }
2119 Ok(Value::Tensor(matrix))
2120}
2121
2122#[runmat_macros::runtime_builtin(
2123 name = "datestr",
2124 builtin_path = "crate::builtins::datetime",
2125 category = "datetime",
2126 summary = "Format date/time inputs as character rows.",
2127 keywords = "datestr,datetime,datenum,date formatting"
2128)]
2129async fn datestr_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
2130 let value = gather_if_needed_async(&value)
2131 .await
2132 .map_err(|err| datetime_error(format!("datestr: {}", err.message())))?;
2133 let rest = gather_args(&rest).await?;
2134 if rest.len() > 1 {
2135 return Err(datetime_error(
2136 "datestr: expected at most one format argument",
2137 ));
2138 }
2139 let format = rest
2140 .first()
2141 .map(|value| scalar_text(value, "datestr format"))
2142 .transpose()?
2143 .unwrap_or_else(|| DEFAULT_DATETIME_FORMAT.to_string());
2144 let serials = match &value {
2145 Value::Tensor(_) => {
2146 if let Ok(datevec) = tensor_from_datevec_like(value.clone(), "datestr") {
2147 datenum_from_datevec_tensor(&datevec, "datestr")?
2148 } else {
2149 numeric_or_datetime_serial_tensor(value, "datestr")?
2150 }
2151 }
2152 _ => numeric_or_datetime_serial_tensor(value, "datestr")?,
2153 };
2154 let mut rows = Vec::with_capacity(serials.data.len());
2155 for serial in &serials.data {
2156 rows.push(format_serial(*serial, &format)?);
2157 }
2158 Ok(Value::CharArray(char_array_from_rows(&rows, "datestr")?))
2159}
2160
2161#[runmat_macros::runtime_builtin(
2162 name = "weekday",
2163 builtin_path = "crate::builtins::datetime",
2164 category = "datetime",
2165 summary = "Return weekday numbers and names for date/time inputs.",
2166 keywords = "weekday,datetime,datenum"
2167)]
2168async fn weekday_builtin(value: Value) -> crate::BuiltinResult<Value> {
2169 let value = gather_if_needed_async(&value)
2170 .await
2171 .map_err(|err| datetime_error(format!("weekday: {}", err.message())))?;
2172 let serials = numeric_or_datetime_serial_tensor(value, "weekday")?;
2173 let mut nums = Vec::with_capacity(serials.data.len());
2174 let mut names = Vec::with_capacity(serials.data.len());
2175 for serial in &serials.data {
2176 let weekday = naive_from_datenum(*serial)?.weekday();
2177 nums.push(f64::from(weekday.num_days_from_sunday()) + 1.0);
2178 names.push(
2179 match weekday {
2180 Weekday::Sun => "Sunday",
2181 Weekday::Mon => "Monday",
2182 Weekday::Tue => "Tuesday",
2183 Weekday::Wed => "Wednesday",
2184 Weekday::Thu => "Thursday",
2185 Weekday::Fri => "Friday",
2186 Weekday::Sat => "Saturday",
2187 }
2188 .to_string(),
2189 );
2190 }
2191 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
2192 let num_value = tensor_or_scalar(nums, shape.clone())?;
2193 let name_value = Value::StringArray(
2194 StringArray::new(names, shape).map_err(|err| datetime_error(format!("weekday: {err}")))?,
2195 );
2196 if let Some(out_count) = crate::output_count::current_output_count() {
2197 return Ok(crate::output_count::output_list_with_padding(
2198 out_count,
2199 vec![num_value, name_value],
2200 ));
2201 }
2202 Ok(num_value)
2203}
2204
2205#[runmat_macros::runtime_builtin(
2206 name = "eomday",
2207 builtin_path = "crate::builtins::datetime",
2208 category = "datetime",
2209 summary = "Return the last day number for month/year pairs.",
2210 keywords = "eomday,end of month,calendar"
2211)]
2212async fn eomday_builtin(year: Value, month: Value) -> crate::BuiltinResult<Value> {
2213 let year = gather_if_needed_async(&year)
2214 .await
2215 .map_err(|err| datetime_error(format!("eomday: {}", err.message())))?;
2216 let month = gather_if_needed_async(&month)
2217 .await
2218 .map_err(|err| datetime_error(format!("eomday: {}", err.message())))?;
2219 let years = component_tensor(year, "year")?;
2220 let months = component_tensor(month, "month")?;
2221 let (year_data, month_data, shape) =
2222 tensor::binary_numeric_tensors(&years, &months, "eomday", BUILTIN_NAME)?;
2223 let mut out = Vec::with_capacity(year_data.len());
2224 for (year, month) in year_data.iter().zip(month_data.iter()) {
2225 let year = round_component(*year, "year", -262_000, 262_000)? as i32;
2226 let month = round_component(*month, "month", 1, 12)? as u32;
2227 out.push(f64::from(days_in_month(year, month)?));
2228 }
2229 tensor_or_scalar(out, shape)
2230}
2231
2232#[runmat_macros::runtime_builtin(
2233 name = "etime",
2234 builtin_path = "crate::builtins::datetime",
2235 category = "datetime",
2236 summary = "Return elapsed seconds between date vectors.",
2237 keywords = "etime,datevec,elapsed time"
2238)]
2239async fn etime_builtin(t2: Value, t1: Value) -> crate::BuiltinResult<Value> {
2240 let t2 = gather_if_needed_async(&t2)
2241 .await
2242 .map_err(|err| datetime_error(format!("etime: {}", err.message())))?;
2243 let t1 = gather_if_needed_async(&t1)
2244 .await
2245 .map_err(|err| datetime_error(format!("etime: {}", err.message())))?;
2246 let t2 = datenum_from_datevec_tensor(&tensor_from_datevec_like(t2, "etime")?, "etime")?;
2247 let t1 = datenum_from_datevec_tensor(&tensor_from_datevec_like(t1, "etime")?, "etime")?;
2248 let (left, right, shape) = tensor::binary_numeric_tensors(&t2, &t1, "etime", BUILTIN_NAME)?;
2249 let out = left
2250 .iter()
2251 .zip(right.iter())
2252 .map(|(a, b)| (a - b) * SECONDS_PER_DAY)
2253 .collect::<Vec<_>>();
2254 tensor_or_scalar(out, shape)
2255}
2256
2257#[runmat_macros::runtime_builtin(
2258 name = "isbetween",
2259 builtin_path = "crate::builtins::datetime",
2260 category = "datetime",
2261 summary = "Return true where values fall between lower and upper bounds.",
2262 keywords = "isbetween,datetime,duration,comparison"
2263)]
2264async fn isbetween_builtin(
2265 value: Value,
2266 lower: Value,
2267 upper: Value,
2268) -> crate::BuiltinResult<Value> {
2269 let value = gather_if_needed_async(&value)
2270 .await
2271 .map_err(|err| datetime_error(format!("isbetween: {}", err.message())))?;
2272 let lower = gather_if_needed_async(&lower)
2273 .await
2274 .map_err(|err| datetime_error(format!("isbetween: {}", err.message())))?;
2275 let upper = gather_if_needed_async(&upper)
2276 .await
2277 .map_err(|err| datetime_error(format!("isbetween: {}", err.message())))?;
2278 let values = numeric_or_datetime_serial_tensor(value, "isbetween")?;
2279 let lower = numeric_or_datetime_serial_tensor(lower, "isbetween")?;
2280 let upper = numeric_or_datetime_serial_tensor(upper, "isbetween")?;
2281 let (values_data, lower_data, upper_data, shape) =
2282 broadcast_three_numeric_tensors(&values, &lower, &upper, "isbetween")?;
2283 let out = values_data
2284 .iter()
2285 .zip(lower_data.iter())
2286 .zip(upper_data.iter())
2287 .map(|((value, lower), upper)| {
2288 if value >= lower && value <= upper {
2289 1.0
2290 } else {
2291 0.0
2292 }
2293 })
2294 .collect::<Vec<_>>();
2295 tensor_or_scalar(out, shape)
2296}
2297
2298#[runmat_macros::runtime_builtin(
2299 name = "calendarDuration",
2300 builtin_path = "crate::builtins::datetime",
2301 category = "datetime",
2302 summary = "Create calendar duration values from calendar components.",
2303 keywords = "calendarDuration,caldays,calmonths,calyears,datetime"
2304)]
2305async fn calendar_duration_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
2306 let args = gather_args(&args).await?;
2307 if args.is_empty() {
2308 return calendar_duration_object_from_components(vec![0.0], vec![0.0], vec![1, 1]);
2309 }
2310 if args.len() == 1 && is_calendar_duration_object(&args[0]) {
2311 return Ok(args[0].clone());
2312 }
2313
2314 let labels = ["years", "months", "days", "hours", "minutes", "seconds"];
2315 let positional = match args.len() {
2316 1 => {
2317 let days = component_tensor(args[0].clone(), "calendarDuration")?;
2318 let shape = tensor::default_shape_for(&days.shape, days.data.len());
2319 return calendar_duration_object_from_components(
2320 vec![0.0; days.data.len()],
2321 days.data,
2322 shape,
2323 );
2324 }
2325 3..=6 => args,
2326 _ => {
2327 return Err(datetime_error(
2328 "calendarDuration: expected no input, days, or Y/M/D[/H/M/S] components",
2329 ))
2330 }
2331 };
2332
2333 let mut arrays = Vec::with_capacity(6);
2334 for (idx, arg) in positional.into_iter().enumerate() {
2335 arrays.push(component_tensor(arg, labels[idx])?);
2336 }
2337 while arrays.len() < 6 {
2338 arrays.push(Tensor::new(vec![0.0], vec![1, 1]).unwrap());
2339 }
2340 let (broadcasted, shape) = broadcast_component_data(&arrays, &labels)?;
2341 let len = broadcasted[0].len();
2342 let mut months = Vec::with_capacity(len);
2343 let mut days = Vec::with_capacity(len);
2344 for idx in 0..len {
2345 let month_value = broadcasted[0][idx] * 12.0 + broadcasted[1][idx];
2346 let day_value = broadcasted[2][idx]
2347 + broadcasted[3][idx] / 24.0
2348 + broadcasted[4][idx] / (24.0 * 60.0)
2349 + broadcasted[5][idx] / SECONDS_PER_DAY;
2350 if !month_value.is_finite() || !day_value.is_finite() {
2351 return Err(datetime_error(
2352 "calendarDuration: resulting calendar duration is outside supported range",
2353 ));
2354 }
2355 months.push(month_value);
2356 days.push(day_value);
2357 }
2358 calendar_duration_object_from_components(months, days, shape)
2359}
2360
2361#[runmat_macros::runtime_builtin(
2362 name = "caldays",
2363 builtin_path = "crate::builtins::datetime",
2364 category = "datetime",
2365 summary = "Create calendar durations from days or convert calendar durations to day counts.",
2366 keywords = "caldays,calendarDuration,datetime"
2367)]
2368async fn caldays_builtin(value: Value) -> crate::BuiltinResult<Value> {
2369 let value = gather_if_needed_async(&value)
2370 .await
2371 .map_err(|err| datetime_error(format!("caldays: {}", err.message())))?;
2372 calendar_duration_unit_value(value, "caldays", 0.0, 1.0)
2373}
2374
2375#[runmat_macros::runtime_builtin(
2376 name = "calweeks",
2377 builtin_path = "crate::builtins::datetime",
2378 category = "datetime",
2379 summary = "Create calendar durations from weeks or convert calendar durations to week counts.",
2380 keywords = "calweeks,calendarDuration,datetime"
2381)]
2382async fn calweeks_builtin(value: Value) -> crate::BuiltinResult<Value> {
2383 let value = gather_if_needed_async(&value)
2384 .await
2385 .map_err(|err| datetime_error(format!("calweeks: {}", err.message())))?;
2386 calendar_duration_unit_value(value, "calweeks", 0.0, 7.0)
2387}
2388
2389#[runmat_macros::runtime_builtin(
2390 name = "calmonths",
2391 builtin_path = "crate::builtins::datetime",
2392 category = "datetime",
2393 summary = "Create calendar durations from months or convert calendar durations to month counts.",
2394 keywords = "calmonths,calendarDuration,datetime"
2395)]
2396async fn calmonths_builtin(value: Value) -> crate::BuiltinResult<Value> {
2397 let value = gather_if_needed_async(&value)
2398 .await
2399 .map_err(|err| datetime_error(format!("calmonths: {}", err.message())))?;
2400 calendar_duration_unit_value(value, "calmonths", 1.0, 0.0)
2401}
2402
2403#[runmat_macros::runtime_builtin(
2404 name = "calquarters",
2405 builtin_path = "crate::builtins::datetime",
2406 category = "datetime",
2407 summary = "Create calendar durations from quarters or convert calendar durations to quarter counts.",
2408 keywords = "calquarters,calendarDuration,datetime"
2409)]
2410async fn calquarters_builtin(value: Value) -> crate::BuiltinResult<Value> {
2411 let value = gather_if_needed_async(&value)
2412 .await
2413 .map_err(|err| datetime_error(format!("calquarters: {}", err.message())))?;
2414 calendar_duration_unit_value(value, "calquarters", 3.0, 0.0)
2415}
2416
2417#[runmat_macros::runtime_builtin(
2418 name = "calyears",
2419 builtin_path = "crate::builtins::datetime",
2420 category = "datetime",
2421 summary = "Create calendar durations from years or convert calendar durations to year counts.",
2422 keywords = "calyears,calendarDuration,datetime"
2423)]
2424async fn calyears_builtin(value: Value) -> crate::BuiltinResult<Value> {
2425 let value = gather_if_needed_async(&value)
2426 .await
2427 .map_err(|err| datetime_error(format!("calyears: {}", err.message())))?;
2428 calendar_duration_unit_value(value, "calyears", 12.0, 0.0)
2429}
2430
2431#[runmat_macros::runtime_builtin(
2432 name = "iscalendarduration",
2433 builtin_path = "crate::builtins::datetime",
2434 category = "datetime",
2435 summary = "Return true for calendarDuration values.",
2436 keywords = "iscalendarduration,calendarDuration,predicate"
2437)]
2438fn iscalendarduration_builtin(value: Value) -> crate::BuiltinResult<Value> {
2439 Ok(Value::Bool(is_calendar_duration_object(&value)))
2440}
2441
2442#[runmat_macros::runtime_builtin(
2443 name = "isbusday",
2444 builtin_path = "crate::builtins::datetime",
2445 category = "datetime",
2446 summary = "Return true where date values are business days.",
2447 keywords = "isbusday,business day,datetime,financial"
2448)]
2449async fn isbusday_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
2450 let value = gather_if_needed_async(&value)
2451 .await
2452 .map_err(|err| datetime_error(format!("isbusday: {}", err.message())))?;
2453 let rest = gather_args(&rest).await?;
2454 if rest.len() > 1 {
2455 return Err(datetime_error(
2456 "isbusday: expected at most one holiday list",
2457 ));
2458 }
2459 let serials = numeric_or_datetime_serial_tensor(value, "isbusday")?;
2460 let (start_key, end_key) = date_key_range(&[&serials])?;
2461 let holidays = holiday_set_from_optional_or_default(
2462 rest.into_iter().next(),
2463 "isbusday",
2464 start_key,
2465 end_key,
2466 )?;
2467 let mut out = Vec::with_capacity(serials.data.len());
2468 for serial in &serials.data {
2469 out.push(
2470 if is_business_day_key(serial_date_key(*serial)?, &holidays)? {
2471 1.0
2472 } else {
2473 0.0
2474 },
2475 );
2476 }
2477 tensor_or_scalar(
2478 out,
2479 tensor::default_shape_for(&serials.shape, serials.data.len()),
2480 )
2481}
2482
2483#[runmat_macros::runtime_builtin(
2484 name = "holidays",
2485 builtin_path = "crate::builtins::datetime",
2486 category = "datetime",
2487 summary = "Return exchange-style market holidays for a year or date range.",
2488 keywords = "holidays,business day,datetime,financial"
2489)]
2490async fn holidays_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
2491 let args = gather_args(&args).await?;
2492 let keys = match args.len() {
2493 0 => {
2494 let year = current_naive_local().year();
2495 let start = key_from_date(NaiveDate::from_ymd_opt(year, 1, 1).unwrap());
2496 let end = key_from_date(NaiveDate::from_ymd_opt(year, 12, 31).unwrap());
2497 holiday_keys_between(start, end)?
2498 }
2499 1 => {
2500 let tensor = tensor_from_numeric(args[0].clone(), "holidays");
2501 if let Ok(tensor) = tensor {
2502 if tensor.data.len() == 1 && (1000.0..=9999.0).contains(&tensor.data[0]) {
2503 let keys = market_holiday_keys_for_year(tensor.data[0].round() as i32)?;
2504 let len = keys.len();
2505 return datetime_object_from_serials(
2506 keys.into_iter().map(|key| key as f64).collect(),
2507 vec![len, 1],
2508 DEFAULT_DATE_FORMAT,
2509 );
2510 }
2511 }
2512 let serials = numeric_or_datetime_serial_tensor(args[0].clone(), "holidays")?;
2513 let year = date_from_key(serial_date_key(serials.data[0])?)?.year();
2514 let start = key_from_date(NaiveDate::from_ymd_opt(year, 1, 1).unwrap());
2515 let end = key_from_date(NaiveDate::from_ymd_opt(year, 12, 31).unwrap());
2516 holiday_keys_between(start, end)?
2517 }
2518 2 => {
2519 let start = numeric_or_datetime_serial_tensor(args[0].clone(), "holidays")?;
2520 let end = numeric_or_datetime_serial_tensor(args[1].clone(), "holidays")?;
2521 if start.data.len() != 1 || end.data.len() != 1 {
2522 return Err(datetime_error(
2523 "holidays: start and end dates must be scalar",
2524 ));
2525 }
2526 holiday_keys_between(
2527 serial_date_key(start.data[0])?,
2528 serial_date_key(end.data[0])?,
2529 )?
2530 }
2531 _ => {
2532 return Err(datetime_error(
2533 "holidays: expected zero, one, or two inputs",
2534 ))
2535 }
2536 };
2537 let len = keys.len();
2538 datetime_object_from_serials(
2539 keys.into_iter().map(|key| key as f64).collect(),
2540 vec![len, 1],
2541 DEFAULT_DATE_FORMAT,
2542 )
2543}
2544
2545#[runmat_macros::runtime_builtin(
2546 name = "busdays",
2547 builtin_path = "crate::builtins::datetime",
2548 category = "datetime",
2549 summary = "Return serial date numbers for business days in a scalar date range.",
2550 keywords = "busdays,business day,datetime,financial"
2551)]
2552async fn busdays_builtin(
2553 start: Value,
2554 end: Value,
2555 rest: Vec<Value>,
2556) -> crate::BuiltinResult<Value> {
2557 let start = gather_if_needed_async(&start)
2558 .await
2559 .map_err(|err| datetime_error(format!("busdays: {}", err.message())))?;
2560 let end = gather_if_needed_async(&end)
2561 .await
2562 .map_err(|err| datetime_error(format!("busdays: {}", err.message())))?;
2563 let rest = gather_args(&rest).await?;
2564 if rest.len() > 1 {
2565 return Err(datetime_error("busdays: expected at most one holiday list"));
2566 }
2567 let start = numeric_or_datetime_serial_tensor(start, "busdays")?;
2568 let end = numeric_or_datetime_serial_tensor(end, "busdays")?;
2569 if start.data.len() != 1 || end.data.len() != 1 {
2570 return Err(datetime_error(
2571 "busdays: start and end dates must be scalar",
2572 ));
2573 }
2574 let mut key = serial_date_key(start.data[0])?;
2575 let end_key = serial_date_key(end.data[0])?;
2576 let span = key
2577 .max(end_key)
2578 .checked_sub(key.min(end_key))
2579 .and_then(|delta| delta.checked_add(1))
2580 .ok_or_else(|| datetime_error("busdays: date range is outside supported range"))?;
2581 if span > MAX_BUSDAYS_OUTPUT_LEN {
2582 return Err(datetime_error(format!(
2583 "busdays: output would exceed {MAX_BUSDAYS_OUTPUT_LEN} dates"
2584 )));
2585 }
2586 let holidays =
2587 holiday_set_from_optional_or_default(rest.into_iter().next(), "busdays", key, end_key)?;
2588 let step = if key <= end_key { 1 } else { -1 };
2589 let mut out = Vec::new();
2590 loop {
2591 if is_business_day_key(key, &holidays)? {
2592 out.push(key as f64);
2593 }
2594 if key == end_key {
2595 break;
2596 }
2597 key = key
2598 .checked_add(step)
2599 .ok_or_else(|| datetime_error("busdays: date range is outside supported range"))?;
2600 }
2601 let len = out.len();
2602 Tensor::new(out, vec![len, 1])
2603 .map(Value::Tensor)
2604 .map_err(|err| datetime_error(format!("busdays: {err}")))
2605}
2606
2607#[runmat_macros::runtime_builtin(
2608 name = "days252bus",
2609 builtin_path = "crate::builtins::datetime",
2610 category = "datetime",
2611 summary = "Count business days between date values using a 252-business-day calendar.",
2612 keywords = "days252bus,business day,datetime,financial"
2613)]
2614async fn days252bus_builtin(
2615 start: Value,
2616 end: Value,
2617 rest: Vec<Value>,
2618) -> crate::BuiltinResult<Value> {
2619 let start = gather_if_needed_async(&start)
2620 .await
2621 .map_err(|err| datetime_error(format!("days252bus: {}", err.message())))?;
2622 let end = gather_if_needed_async(&end)
2623 .await
2624 .map_err(|err| datetime_error(format!("days252bus: {}", err.message())))?;
2625 let rest = gather_args(&rest).await?;
2626 if rest.len() > 1 {
2627 return Err(datetime_error(
2628 "days252bus: expected at most one holiday list",
2629 ));
2630 }
2631 let starts = numeric_or_datetime_serial_tensor(start, "days252bus")?;
2632 let ends = numeric_or_datetime_serial_tensor(end, "days252bus")?;
2633 let (start_key, end_key) = date_key_range(&[&starts, &ends])?;
2634 let holidays = holiday_set_from_optional_or_default(
2635 rest.into_iter().next(),
2636 "days252bus",
2637 start_key,
2638 end_key,
2639 )?;
2640 let (start_data, end_data, shape) =
2641 tensor::binary_numeric_tensors(&starts, &ends, "days252bus", BUILTIN_NAME)?;
2642 let counts = start_data
2643 .iter()
2644 .zip(end_data.iter())
2645 .map(|(start, end)| {
2646 Ok(
2647 count_business_days(serial_date_key(*start)?, serial_date_key(*end)?, &holidays)?
2648 as f64,
2649 )
2650 })
2651 .collect::<BuiltinResult<Vec<_>>>()?;
2652 tensor_or_scalar(counts, shape)
2653}
2654
2655#[runmat_macros::runtime_builtin(
2656 name = "daysdif",
2657 builtin_path = "crate::builtins::datetime",
2658 category = "datetime",
2659 summary = "Return date differences using actual or 30/360 day-count bases.",
2660 keywords = "daysdif,date difference,datetime,financial"
2661)]
2662async fn daysdif_builtin(
2663 start: Value,
2664 end: Value,
2665 rest: Vec<Value>,
2666) -> crate::BuiltinResult<Value> {
2667 let start = gather_if_needed_async(&start)
2668 .await
2669 .map_err(|err| datetime_error(format!("daysdif: {}", err.message())))?;
2670 let end = gather_if_needed_async(&end)
2671 .await
2672 .map_err(|err| datetime_error(format!("daysdif: {}", err.message())))?;
2673 let rest = gather_args(&rest).await?;
2674 if rest.len() > 1 {
2675 return Err(datetime_error(
2676 "daysdif: expected at most one basis argument",
2677 ));
2678 }
2679 let basis = rest
2680 .first()
2681 .map(|value| tensor_from_numeric(value.clone(), "daysdif"))
2682 .transpose()?
2683 .and_then(|tensor| tensor.data.first().copied())
2684 .unwrap_or(0.0)
2685 .round() as i64;
2686 let starts = numeric_or_datetime_serial_tensor(start, "daysdif")?;
2687 let ends = numeric_or_datetime_serial_tensor(end, "daysdif")?;
2688 let (start_data, end_data, shape) =
2689 tensor::binary_numeric_tensors(&starts, &ends, "daysdif", BUILTIN_NAME)?;
2690 let out = start_data
2691 .iter()
2692 .zip(end_data.iter())
2693 .map(|(start, end)| {
2694 let start_key = serial_date_key(*start)?;
2695 let end_key = serial_date_key(*end)?;
2696 if basis == 1 {
2697 let s = date_from_key(start_key)?;
2698 let e = date_from_key(end_key)?;
2699 let sd = s.day().min(30) as i32;
2700 let ed = if sd == 30 { e.day().min(30) } else { e.day() } as i32;
2701 Ok(f64::from(
2702 (e.year() - s.year()) * 360
2703 + (e.month() as i32 - s.month() as i32) * 30
2704 + (ed - sd),
2705 ))
2706 } else {
2707 Ok((end_key - start_key) as f64)
2708 }
2709 })
2710 .collect::<BuiltinResult<Vec<_>>>()?;
2711 tensor_or_scalar(out, shape)
2712}
2713
2714#[runmat_macros::runtime_builtin(
2715 name = "fbusdate",
2716 builtin_path = "crate::builtins::datetime",
2717 category = "datetime",
2718 summary = "Return first business day serial date numbers for month/year pairs.",
2719 keywords = "fbusdate,business day,datetime,financial"
2720)]
2721async fn fbusdate_builtin(
2722 year: Value,
2723 month: Value,
2724 rest: Vec<Value>,
2725) -> crate::BuiltinResult<Value> {
2726 let year = gather_if_needed_async(&year)
2727 .await
2728 .map_err(|err| datetime_error(format!("fbusdate: {}", err.message())))?;
2729 let month = gather_if_needed_async(&month)
2730 .await
2731 .map_err(|err| datetime_error(format!("fbusdate: {}", err.message())))?;
2732 let rest = gather_args(&rest).await?;
2733 if rest.len() > 1 {
2734 return Err(datetime_error(
2735 "fbusdate: expected at most one holiday list",
2736 ));
2737 }
2738 let years = component_tensor(year, "fbusdate year")?;
2739 let months = component_tensor(month, "fbusdate month")?;
2740 let (year_data, month_data, shape) =
2741 tensor::binary_numeric_tensors(&years, &months, "fbusdate", BUILTIN_NAME)?;
2742 let mut min_key = i64::MAX;
2743 let mut max_key = i64::MIN;
2744 for (year, month) in year_data.iter().zip(month_data.iter()) {
2745 let year = round_component(*year, "year", -262_000, 262_000)? as i32;
2746 let month = round_component(*month, "month", 1, 12)? as u32;
2747 min_key = min_key.min(key_from_date(
2748 NaiveDate::from_ymd_opt(year, month, 1)
2749 .ok_or_else(|| datetime_error("fbusdate: invalid year/month"))?,
2750 ));
2751 max_key = max_key.max(key_from_date(
2752 NaiveDate::from_ymd_opt(year, month, days_in_month(year, month)?)
2753 .ok_or_else(|| datetime_error("fbusdate: invalid year/month"))?,
2754 ));
2755 }
2756 let holidays = holiday_set_from_optional_or_default(
2757 rest.into_iter().next(),
2758 "fbusdate",
2759 min_key,
2760 max_key,
2761 )?;
2762 let out = year_data
2763 .iter()
2764 .zip(month_data.iter())
2765 .map(|(year, month)| {
2766 Ok(first_business_day_key(
2767 round_component(*year, "year", -262_000, 262_000)? as i32,
2768 round_component(*month, "month", 1, 12)? as u32,
2769 &holidays,
2770 )? as f64)
2771 })
2772 .collect::<BuiltinResult<Vec<_>>>()?;
2773 tensor_or_scalar(out, shape)
2774}
2775
2776#[runmat_macros::runtime_builtin(
2777 name = "lbusdate",
2778 builtin_path = "crate::builtins::datetime",
2779 category = "datetime",
2780 summary = "Return last business day serial date numbers for month/year pairs.",
2781 keywords = "lbusdate,business day,datetime,financial"
2782)]
2783async fn lbusdate_builtin(
2784 year: Value,
2785 month: Value,
2786 rest: Vec<Value>,
2787) -> crate::BuiltinResult<Value> {
2788 let year = gather_if_needed_async(&year)
2789 .await
2790 .map_err(|err| datetime_error(format!("lbusdate: {}", err.message())))?;
2791 let month = gather_if_needed_async(&month)
2792 .await
2793 .map_err(|err| datetime_error(format!("lbusdate: {}", err.message())))?;
2794 let rest = gather_args(&rest).await?;
2795 if rest.len() > 1 {
2796 return Err(datetime_error(
2797 "lbusdate: expected at most one holiday list",
2798 ));
2799 }
2800 let years = component_tensor(year, "lbusdate year")?;
2801 let months = component_tensor(month, "lbusdate month")?;
2802 let (year_data, month_data, shape) =
2803 tensor::binary_numeric_tensors(&years, &months, "lbusdate", BUILTIN_NAME)?;
2804 let mut min_key = i64::MAX;
2805 let mut max_key = i64::MIN;
2806 for (year, month) in year_data.iter().zip(month_data.iter()) {
2807 let year = round_component(*year, "year", -262_000, 262_000)? as i32;
2808 let month = round_component(*month, "month", 1, 12)? as u32;
2809 min_key = min_key.min(key_from_date(
2810 NaiveDate::from_ymd_opt(year, month, 1)
2811 .ok_or_else(|| datetime_error("lbusdate: invalid year/month"))?,
2812 ));
2813 max_key = max_key.max(key_from_date(
2814 NaiveDate::from_ymd_opt(year, month, days_in_month(year, month)?)
2815 .ok_or_else(|| datetime_error("lbusdate: invalid year/month"))?,
2816 ));
2817 }
2818 let holidays = holiday_set_from_optional_or_default(
2819 rest.into_iter().next(),
2820 "lbusdate",
2821 min_key,
2822 max_key,
2823 )?;
2824 let out = year_data
2825 .iter()
2826 .zip(month_data.iter())
2827 .map(|(year, month)| {
2828 Ok(last_business_day_key(
2829 round_component(*year, "year", -262_000, 262_000)? as i32,
2830 round_component(*month, "month", 1, 12)? as u32,
2831 &holidays,
2832 )? as f64)
2833 })
2834 .collect::<BuiltinResult<Vec<_>>>()?;
2835 tensor_or_scalar(out, shape)
2836}
2837
2838#[runmat_macros::runtime_builtin(
2839 name = "datetick",
2840 builtin_path = "crate::builtins::datetime",
2841 category = "plotting",
2842 summary = "Accept MATLAB date-axis formatting calls for compatibility.",
2843 keywords = "datetick,plot,date axis"
2844)]
2845async fn datetick_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
2846 let _args = gather_args(&args).await?;
2847 Ok(Value::Num(0.0))
2848}
2849
2850#[runmat_macros::runtime_builtin(
2851 name = "datetime.subsref",
2852 descriptor(crate::builtins::datetime::DATETIME_SUBSREF_DESCRIPTOR),
2853 builtin_path = "crate::builtins::datetime"
2854)]
2855async fn datetime_subsref(obj: Value, kind: String, payload: Value) -> crate::BuiltinResult<Value> {
2856 match kind.as_str() {
2857 OBJECT_INDEX_PAREN => datetime_indexing(obj, payload).await,
2858 OBJECT_INDEX_MEMBER => {
2859 let Value::Object(object) = obj else {
2860 return Err(datetime_error(
2861 "datetime.subsref: receiver must be a datetime object",
2862 ));
2863 };
2864 let field = scalar_text(&payload, "field selector")?;
2865 match field.as_str() {
2866 FORMAT_FIELD => Ok(Value::String(format_for_object(&object))),
2867 _ => Err(datetime_error(format!(
2868 "datetime.subsref: unsupported datetime property '{field}'"
2869 ))),
2870 }
2871 }
2872 other => Err(datetime_error(format!(
2873 "datetime.subsref: unsupported indexing kind '{other}'"
2874 ))),
2875 }
2876}
2877
2878#[runmat_macros::runtime_builtin(
2879 name = "datetime.subsasgn",
2880 descriptor(crate::builtins::datetime::DATETIME_SUBSASGN_DESCRIPTOR),
2881 builtin_path = "crate::builtins::datetime"
2882)]
2883async fn datetime_subsasgn(
2884 obj: Value,
2885 kind: String,
2886 payload: Value,
2887 rhs: Value,
2888) -> crate::BuiltinResult<Value> {
2889 let Value::Object(mut object) = obj else {
2890 return Err(datetime_error(
2891 "datetime.subsasgn: receiver must be a datetime object",
2892 ));
2893 };
2894 match kind.as_str() {
2895 OBJECT_INDEX_MEMBER => {
2896 let field = scalar_text(&payload, "field selector")?;
2897 match field.as_str() {
2898 FORMAT_FIELD => {
2899 let text = scalar_text(&rhs, "Format value")?;
2900 object
2901 .properties
2902 .insert(FORMAT_FIELD.to_string(), Value::String(text));
2903 Ok(Value::Object(object))
2904 }
2905 _ => Err(datetime_error(format!(
2906 "datetime.subsasgn: unsupported datetime property '{field}'"
2907 ))),
2908 }
2909 }
2910 _ => Err(datetime_error(format!(
2911 "datetime.subsasgn: unsupported indexing kind '{kind}'"
2912 ))),
2913 }
2914}
2915
2916fn datetime_binary_serials(
2917 lhs: Value,
2918 rhs: Value,
2919 context: &str,
2920) -> BuiltinResult<(Tensor, Tensor, Vec<usize>, String)> {
2921 let lhs_serials = serials_from_datetime_value(&lhs)?;
2922 let rhs_serials = match &rhs {
2923 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => serial_tensor_for_object(obj)?,
2924 _ => serial_tensor_from_value(rhs, context)?,
2925 };
2926 let (left, right, shape) =
2927 tensor::binary_numeric_tensors(&lhs_serials, &rhs_serials, context, BUILTIN_NAME)?;
2928 let left_tensor = Tensor::new(left, shape.clone())
2929 .map_err(|err| datetime_error(format!("{context}: {err}")))?;
2930 let right_tensor = Tensor::new(right, shape.clone())
2931 .map_err(|err| datetime_error(format!("{context}: {err}")))?;
2932 Ok((
2933 left_tensor,
2934 right_tensor,
2935 shape,
2936 datetime_format_from_value(&lhs),
2937 ))
2938}
2939
2940fn compare_datetime(
2941 lhs: Value,
2942 rhs: Value,
2943 op: &str,
2944 cmp: impl Fn(f64, f64) -> bool,
2945) -> BuiltinResult<Value> {
2946 let (left, right, shape, _) = datetime_binary_serials(lhs, rhs, op)?;
2947 let out = left
2948 .data
2949 .iter()
2950 .zip(right.data.iter())
2951 .map(|(a, b)| if cmp(*a, *b) { 1.0 } else { 0.0 })
2952 .collect::<Vec<_>>();
2953 tensor_or_scalar(out, shape)
2954}
2955
2956#[runmat_macros::runtime_builtin(
2957 name = "datetime.eq",
2958 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
2959 builtin_path = "crate::builtins::datetime"
2960)]
2961async fn datetime_eq(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
2962 compare_datetime(lhs, rhs, "eq", |a, b| (a - b).abs() <= 1e-12)
2963}
2964
2965#[runmat_macros::runtime_builtin(
2966 name = "datetime.ne",
2967 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
2968 builtin_path = "crate::builtins::datetime"
2969)]
2970async fn datetime_ne(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
2971 compare_datetime(lhs, rhs, "ne", |a, b| (a - b).abs() > 1e-12)
2972}
2973
2974#[runmat_macros::runtime_builtin(
2975 name = "datetime.lt",
2976 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
2977 builtin_path = "crate::builtins::datetime"
2978)]
2979async fn datetime_lt(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
2980 compare_datetime(lhs, rhs, "lt", |a, b| a < b)
2981}
2982
2983#[runmat_macros::runtime_builtin(
2984 name = "datetime.le",
2985 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
2986 builtin_path = "crate::builtins::datetime"
2987)]
2988async fn datetime_le(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
2989 compare_datetime(lhs, rhs, "le", |a, b| a <= b)
2990}
2991
2992#[runmat_macros::runtime_builtin(
2993 name = "datetime.gt",
2994 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
2995 builtin_path = "crate::builtins::datetime"
2996)]
2997async fn datetime_gt(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
2998 compare_datetime(lhs, rhs, "gt", |a, b| a > b)
2999}
3000
3001#[runmat_macros::runtime_builtin(
3002 name = "datetime.ge",
3003 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
3004 builtin_path = "crate::builtins::datetime"
3005)]
3006async fn datetime_ge(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
3007 compare_datetime(lhs, rhs, "ge", |a, b| a >= b)
3008}
3009
3010#[runmat_macros::runtime_builtin(
3011 name = "datetime.plus",
3012 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
3013 builtin_path = "crate::builtins::datetime"
3014)]
3015async fn datetime_plus(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
3016 let lhs_serials = serials_from_datetime_value(&lhs)?;
3017 if is_calendar_duration_object(&rhs) {
3018 let (months, days) = calendar_duration_tensors_from_value(&rhs)?;
3019 let (serials, shape) =
3020 apply_calendar_duration_to_serials(&lhs_serials, &months, &days, 1.0, "plus")?;
3021 return datetime_object_from_serials(serials, shape, datetime_format_from_value(&lhs));
3022 }
3023 let rhs_numeric = if crate::builtins::duration::is_duration_object(&rhs) {
3024 crate::builtins::duration::duration_tensor_from_duration_value(&rhs)?
3025 } else {
3026 serial_tensor_from_value(rhs, "plus")?
3027 };
3028 let (left, right, shape) =
3029 tensor::binary_numeric_tensors(&lhs_serials, &rhs_numeric, "plus", BUILTIN_NAME)?;
3030 let serials = left
3031 .iter()
3032 .zip(right.iter())
3033 .map(|(a, b)| a + b)
3034 .collect::<Vec<_>>();
3035 datetime_object_from_serials(serials, shape, datetime_format_from_value(&lhs))
3036}
3037
3038#[runmat_macros::runtime_builtin(
3039 name = "datetime.minus",
3040 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
3041 builtin_path = "crate::builtins::datetime"
3042)]
3043async fn datetime_minus(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
3044 let lhs_serials = serials_from_datetime_value(&lhs)?;
3045 match &rhs {
3046 _ if is_calendar_duration_object(&rhs) => {
3047 let (months, days) = calendar_duration_tensors_from_value(&rhs)?;
3048 let (serials, shape) =
3049 apply_calendar_duration_to_serials(&lhs_serials, &months, &days, -1.0, "minus")?;
3050 datetime_object_from_serials(serials, shape, datetime_format_from_value(&lhs))
3051 }
3052 _ if crate::builtins::duration::is_duration_object(&rhs) => {
3053 let rhs_days = crate::builtins::duration::duration_tensor_from_duration_value(&rhs)?;
3054 let (left, right, shape) =
3055 tensor::binary_numeric_tensors(&lhs_serials, &rhs_days, "minus", BUILTIN_NAME)?;
3056 let serials = left
3057 .iter()
3058 .zip(right.iter())
3059 .map(|(a, b)| a - b)
3060 .collect::<Vec<_>>();
3061 datetime_object_from_serials(serials, shape, datetime_format_from_value(&lhs))
3062 }
3063 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => {
3064 let rhs_serials = serial_tensor_for_object(obj)?;
3065 let (left, right, shape) =
3066 tensor::binary_numeric_tensors(&lhs_serials, &rhs_serials, "minus", BUILTIN_NAME)?;
3067 let deltas = left
3068 .iter()
3069 .zip(right.iter())
3070 .map(|(a, b)| a - b)
3071 .collect::<Vec<_>>();
3072 tensor_or_scalar(deltas, shape)
3073 }
3074 _ => {
3075 let rhs_numeric = serial_tensor_from_value(rhs, "minus")?;
3076 let (left, right, shape) =
3077 tensor::binary_numeric_tensors(&lhs_serials, &rhs_numeric, "minus", BUILTIN_NAME)?;
3078 let serials = left
3079 .iter()
3080 .zip(right.iter())
3081 .map(|(a, b)| a - b)
3082 .collect::<Vec<_>>();
3083 datetime_object_from_serials(serials, shape, datetime_format_from_value(&lhs))
3084 }
3085 }
3086}
3087
3088fn combine_calendar_durations(
3089 lhs: &Value,
3090 rhs: &Value,
3091 sign: f64,
3092 context: &str,
3093) -> BuiltinResult<Value> {
3094 let (lhs_months, lhs_days) = calendar_duration_tensors_from_value(lhs)?;
3095 let (rhs_months, rhs_days) = calendar_duration_tensors_from_value(rhs)?;
3096 let (left_months, right_months, shape) =
3097 tensor::binary_numeric_tensors(&lhs_months, &rhs_months, context, BUILTIN_NAME)?;
3098 let lhs_days_shape = tensor::default_shape_for(&lhs_days.shape, lhs_days.data.len());
3099 let rhs_days_shape = tensor::default_shape_for(&rhs_days.shape, rhs_days.data.len());
3100 let lhs_day_tensor = Tensor::new(lhs_days.data, lhs_days_shape)
3101 .map_err(|err| datetime_error(format!("{context}: {err}")))?;
3102 let rhs_day_tensor = Tensor::new(rhs_days.data, rhs_days_shape)
3103 .map_err(|err| datetime_error(format!("{context}: {err}")))?;
3104 let (left_days, right_days, day_shape) =
3105 tensor::binary_numeric_tensors(&lhs_day_tensor, &rhs_day_tensor, context, BUILTIN_NAME)?;
3106 if day_shape != shape {
3107 return Err(datetime_error(format!(
3108 "{context}: calendarDuration operands must have matching component sizes"
3109 )));
3110 }
3111 let months = left_months
3112 .iter()
3113 .zip(right_months.iter())
3114 .map(|(left, right)| left + sign * right)
3115 .collect::<Vec<_>>();
3116 let days = left_days
3117 .iter()
3118 .zip(right_days.iter())
3119 .map(|(left, right)| left + sign * right)
3120 .collect::<Vec<_>>();
3121 calendar_duration_object_from_components(months, days, shape)
3122}
3123
3124#[runmat_macros::runtime_builtin(
3125 name = "calendarDuration.plus",
3126 builtin_path = "crate::builtins::datetime"
3127)]
3128async fn calendar_duration_plus(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
3129 if is_datetime_object(&rhs) {
3130 let (months, days) = calendar_duration_tensors_from_value(&lhs)?;
3131 let rhs_serials = serials_from_datetime_value(&rhs)?;
3132 let (serials, shape) =
3133 apply_calendar_duration_to_serials(&rhs_serials, &months, &days, 1.0, "plus")?;
3134 return datetime_object_from_serials(serials, shape, datetime_format_from_value(&rhs));
3135 }
3136 combine_calendar_durations(&lhs, &rhs, 1.0, "plus")
3137}
3138
3139#[runmat_macros::runtime_builtin(
3140 name = "calendarDuration.minus",
3141 builtin_path = "crate::builtins::datetime"
3142)]
3143async fn calendar_duration_minus(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
3144 combine_calendar_durations(&lhs, &rhs, -1.0, "minus")
3145}
3146
3147#[runmat_macros::runtime_builtin(
3148 name = "calendarDuration.eq",
3149 builtin_path = "crate::builtins::datetime"
3150)]
3151async fn calendar_duration_eq(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
3152 let (lhs_months, lhs_days) = calendar_duration_tensors_from_value(&lhs)?;
3153 let (rhs_months, rhs_days) = calendar_duration_tensors_from_value(&rhs)?;
3154 let (left_months, right_months, shape) =
3155 tensor::binary_numeric_tensors(&lhs_months, &rhs_months, "eq", BUILTIN_NAME)?;
3156 let (left_days, right_days, day_shape) =
3157 tensor::binary_numeric_tensors(&lhs_days, &rhs_days, "eq", BUILTIN_NAME)?;
3158 if day_shape != shape {
3159 return Err(datetime_error(
3160 "eq: calendarDuration operands must have matching component sizes",
3161 ));
3162 }
3163 let out = left_months
3164 .iter()
3165 .zip(right_months.iter())
3166 .zip(left_days.iter().zip(right_days.iter()))
3167 .map(|((lm, rm), (ld, rd))| {
3168 if (lm - rm).abs() <= 1e-12 && (ld - rd).abs() <= 1e-12 {
3169 1.0
3170 } else {
3171 0.0
3172 }
3173 })
3174 .collect::<Vec<_>>();
3175 tensor_or_scalar(out, shape)
3176}
3177
3178#[runmat_macros::runtime_builtin(
3179 name = "calendarDuration.ne",
3180 builtin_path = "crate::builtins::datetime"
3181)]
3182async fn calendar_duration_ne(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
3183 let eq = calendar_duration_eq(lhs, rhs).await?;
3184 match eq {
3185 Value::Num(value) => Ok(Value::Num(if value == 0.0 { 1.0 } else { 0.0 })),
3186 Value::Tensor(tensor) => Ok(Value::Tensor(
3187 Tensor::new(
3188 tensor
3189 .data
3190 .into_iter()
3191 .map(|value| if value == 0.0 { 1.0 } else { 0.0 })
3192 .collect(),
3193 tensor.shape,
3194 )
3195 .map_err(|err| datetime_error(format!("ne: {err}")))?,
3196 )),
3197 other => Ok(other),
3198 }
3199}
3200
3201#[derive(Clone, Copy, PartialEq, Eq)]
3202enum DateShiftBoundary {
3203 Start,
3204 End,
3205 Nearest,
3206 DayOfWeek,
3207}
3208
3209impl DateShiftBoundary {
3210 fn parse(value: &Value) -> BuiltinResult<Self> {
3211 let text = scalar_text(value, "dateshift boundary")?;
3212 match text.trim().to_ascii_lowercase().as_str() {
3213 "start" => Ok(Self::Start),
3214 "end" => Ok(Self::End),
3215 "nearest" => Ok(Self::Nearest),
3216 "dayofweek" => Ok(Self::DayOfWeek),
3217 other => Err(datetime_error(format!(
3218 "dateshift: unsupported boundary '{other}'"
3219 ))),
3220 }
3221 }
3222}
3223
3224#[derive(Clone, Copy)]
3225enum DateShiftUnit {
3226 Year,
3227 Quarter,
3228 Month,
3229 Week,
3230 Day,
3231 Hour,
3232 Minute,
3233 Second,
3234}
3235
3236impl DateShiftUnit {
3237 fn parse(value: &Value) -> BuiltinResult<Self> {
3238 let text = scalar_text(value, "dateshift unit")?;
3239 match text.trim().to_ascii_lowercase().as_str() {
3240 "year" | "years" => Ok(Self::Year),
3241 "quarter" | "quarters" => Ok(Self::Quarter),
3242 "month" | "months" => Ok(Self::Month),
3243 "week" | "weeks" => Ok(Self::Week),
3244 "day" | "days" => Ok(Self::Day),
3245 "hour" | "hours" => Ok(Self::Hour),
3246 "minute" | "minutes" => Ok(Self::Minute),
3247 "second" | "seconds" => Ok(Self::Second),
3248 other => Err(datetime_error(format!(
3249 "dateshift: unsupported unit '{other}'"
3250 ))),
3251 }
3252 }
3253}
3254
3255fn parse_weekday(value: &Value) -> BuiltinResult<Weekday> {
3256 match value {
3257 Value::Num(n) if n.is_finite() && (*n - n.round()).abs() <= f64::EPSILON => {
3258 weekday_from_matlab_index(n.round() as i64)
3259 }
3260 Value::Int(i) => weekday_from_matlab_index(i.to_i64()),
3261 _ => {
3262 let text = scalar_text(value, "weekday")?;
3263 match text.trim().to_ascii_lowercase().as_str() {
3264 "sun" | "sunday" => Ok(Weekday::Sun),
3265 "mon" | "monday" => Ok(Weekday::Mon),
3266 "tue" | "tues" | "tuesday" => Ok(Weekday::Tue),
3267 "wed" | "wednesday" => Ok(Weekday::Wed),
3268 "thu" | "thur" | "thurs" | "thursday" => Ok(Weekday::Thu),
3269 "fri" | "friday" => Ok(Weekday::Fri),
3270 "sat" | "saturday" => Ok(Weekday::Sat),
3271 other => Err(datetime_error(format!(
3272 "dateshift: unsupported weekday '{other}'"
3273 ))),
3274 }
3275 }
3276 }
3277}
3278
3279fn weekday_from_matlab_index(index: i64) -> BuiltinResult<Weekday> {
3280 match index {
3281 1 => Ok(Weekday::Sun),
3282 2 => Ok(Weekday::Mon),
3283 3 => Ok(Weekday::Tue),
3284 4 => Ok(Weekday::Wed),
3285 5 => Ok(Weekday::Thu),
3286 6 => Ok(Weekday::Fri),
3287 7 => Ok(Weekday::Sat),
3288 _ => Err(datetime_error(
3289 "dateshift: numeric weekdays must be in the range 1..7",
3290 )),
3291 }
3292}
3293
3294fn midnight(date: NaiveDate) -> NaiveDateTime {
3295 date.and_hms_opt(0, 0, 0).unwrap()
3296}
3297
3298fn start_of_week(value: NaiveDateTime, week_start: Weekday) -> NaiveDateTime {
3299 let current = value.weekday().num_days_from_monday() as i64;
3300 let start = week_start.num_days_from_monday() as i64;
3301 let delta = (current - start).rem_euclid(7);
3302 midnight(value.date() - Duration::days(delta))
3303}
3304
3305fn start_of_unit(value: NaiveDateTime, unit: DateShiftUnit, week_start: Weekday) -> NaiveDateTime {
3306 match unit {
3307 DateShiftUnit::Year => midnight(NaiveDate::from_ymd_opt(value.year(), 1, 1).unwrap()),
3308 DateShiftUnit::Quarter => {
3309 let month = ((value.month() - 1) / 3) * 3 + 1;
3310 midnight(NaiveDate::from_ymd_opt(value.year(), month, 1).unwrap())
3311 }
3312 DateShiftUnit::Month => {
3313 midnight(NaiveDate::from_ymd_opt(value.year(), value.month(), 1).unwrap())
3314 }
3315 DateShiftUnit::Week => start_of_week(value, week_start),
3316 DateShiftUnit::Day => midnight(value.date()),
3317 DateShiftUnit::Hour => value
3318 .date()
3319 .and_hms_nano_opt(value.hour(), 0, 0, 0)
3320 .unwrap(),
3321 DateShiftUnit::Minute => value
3322 .date()
3323 .and_hms_nano_opt(value.hour(), value.minute(), 0, 0)
3324 .unwrap(),
3325 DateShiftUnit::Second => value
3326 .date()
3327 .and_hms_nano_opt(value.hour(), value.minute(), value.second(), 0)
3328 .unwrap(),
3329 }
3330}
3331
3332fn add_months(year: i32, month: u32, delta: u32) -> (i32, u32) {
3333 let zero_based = year as i64 * 12 + i64::from(month - 1) + i64::from(delta);
3334 let out_year = zero_based.div_euclid(12) as i32;
3335 let out_month = zero_based.rem_euclid(12) as u32 + 1;
3336 (out_year, out_month)
3337}
3338
3339fn next_unit_start(start: NaiveDateTime, unit: DateShiftUnit) -> NaiveDateTime {
3340 match unit {
3341 DateShiftUnit::Year => midnight(NaiveDate::from_ymd_opt(start.year() + 1, 1, 1).unwrap()),
3342 DateShiftUnit::Quarter => {
3343 let (year, month) = add_months(start.year(), start.month(), 3);
3344 midnight(NaiveDate::from_ymd_opt(year, month, 1).unwrap())
3345 }
3346 DateShiftUnit::Month => {
3347 let (year, month) = add_months(start.year(), start.month(), 1);
3348 midnight(NaiveDate::from_ymd_opt(year, month, 1).unwrap())
3349 }
3350 DateShiftUnit::Week => start + Duration::days(7),
3351 DateShiftUnit::Day => start + Duration::days(1),
3352 DateShiftUnit::Hour => start + Duration::hours(1),
3353 DateShiftUnit::Minute => start + Duration::minutes(1),
3354 DateShiftUnit::Second => start + Duration::seconds(1),
3355 }
3356}
3357
3358fn shift_naive_datetime(
3359 value: NaiveDateTime,
3360 boundary: DateShiftBoundary,
3361 unit: DateShiftUnit,
3362 week_start: Weekday,
3363) -> NaiveDateTime {
3364 let start = start_of_unit(value, unit, week_start);
3365 match boundary {
3366 DateShiftBoundary::Start => start,
3367 DateShiftBoundary::End => next_unit_start(start, unit) - Duration::milliseconds(1),
3368 DateShiftBoundary::Nearest => {
3369 let next = next_unit_start(start, unit);
3370 if value - start <= next - value {
3371 start
3372 } else {
3373 next
3374 }
3375 }
3376 DateShiftBoundary::DayOfWeek => value,
3377 }
3378}
3379
3380fn shift_to_dayofweek(value: NaiveDateTime, weekday: Weekday) -> NaiveDateTime {
3381 let current = value.weekday().num_days_from_monday() as i64;
3382 let target = weekday.num_days_from_monday() as i64;
3383 let delta = (target - current).rem_euclid(7);
3384 midnight(value.date() + Duration::days(delta))
3385}
3386
3387#[runmat_macros::runtime_builtin(
3388 name = "dateshift",
3389 descriptor(crate::builtins::datetime::DATESHIFT_DESCRIPTOR),
3390 builtin_path = "crate::builtins::datetime",
3391 category = "datetime",
3392 summary = "Shift datetime values to calendar or clock boundaries.",
3393 keywords = "dateshift,datetime,start,end,nearest,week,month,year",
3394 related = "datetime,year,month,day"
3395)]
3396async fn dateshift_builtin(
3397 value: Value,
3398 boundary: Value,
3399 unit: Value,
3400 rest: Vec<Value>,
3401) -> crate::BuiltinResult<Value> {
3402 let value = gather_if_needed_async(&value)
3403 .await
3404 .map_err(|err| datetime_error(format!("dateshift: {}", err.message())))?;
3405 let boundary = gather_if_needed_async(&boundary)
3406 .await
3407 .map_err(|err| datetime_error(format!("dateshift: {}", err.message())))?;
3408 let unit = gather_if_needed_async(&unit)
3409 .await
3410 .map_err(|err| datetime_error(format!("dateshift: {}", err.message())))?;
3411 let rest = gather_args(&rest).await?;
3412 let serials = serials_from_datetime_value(&value)?;
3413 let format = datetime_format_from_value(&value);
3414 let boundary = DateShiftBoundary::parse(&boundary)?;
3415
3416 let mut out = Vec::with_capacity(serials.data.len());
3417 if boundary == DateShiftBoundary::DayOfWeek {
3418 if !rest.is_empty() {
3419 return Err(datetime_error(
3420 "dateshift: dayofweek boundary does not accept extra arguments",
3421 ));
3422 }
3423 let weekday = parse_weekday(&unit)?;
3424 for serial in &serials.data {
3425 out.push(datenum_from_naive(shift_to_dayofweek(
3426 naive_from_datenum(*serial)?,
3427 weekday,
3428 )));
3429 }
3430 } else {
3431 let unit = DateShiftUnit::parse(&unit)?;
3432 let week_start = if matches!(unit, DateShiftUnit::Week) {
3433 if rest.len() > 1 {
3434 return Err(datetime_error(
3435 "dateshift: week unit accepts at most one weekday argument",
3436 ));
3437 }
3438 rest.first()
3439 .map(parse_weekday)
3440 .transpose()?
3441 .unwrap_or(Weekday::Mon)
3442 } else {
3443 if !rest.is_empty() {
3444 return Err(datetime_error(
3445 "dateshift: extra arguments are only supported for week units",
3446 ));
3447 }
3448 Weekday::Mon
3449 };
3450 for serial in &serials.data {
3451 out.push(datenum_from_naive(shift_naive_datetime(
3452 naive_from_datenum(*serial)?,
3453 boundary,
3454 unit,
3455 week_start,
3456 )));
3457 }
3458 }
3459
3460 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
3461 datetime_object_from_serials(out, shape, format)
3462}
3463
3464pub fn datetime_char_array(value: &Value) -> BuiltinResult<Option<CharArray>> {
3465 let Some(array) = datetime_string_array(value)? else {
3466 return Ok(None);
3467 };
3468 let width = array
3469 .data
3470 .iter()
3471 .map(|s| s.chars().count())
3472 .max()
3473 .unwrap_or(0);
3474 let rows = array.data.len();
3475 let mut data = vec![' '; rows * width];
3476 for (row, text) in array.data.iter().enumerate() {
3477 for (col, ch) in text.chars().enumerate() {
3478 data[row * width + col] = ch;
3479 }
3480 }
3481 let out = CharArray::new(data, rows, width)
3482 .map_err(|err| datetime_error(format!("datetime: {err}")))?;
3483 Ok(Some(out))
3484}
3485
3486#[cfg(test)]
3487mod tests {
3488 use super::*;
3489
3490 fn run_datetime(args: Vec<Value>) -> Value {
3491 futures::executor::block_on(datetime_builtin(args)).expect("datetime")
3492 }
3493
3494 fn as_datetime(value: Value) -> ObjectInstance {
3495 match value {
3496 Value::Object(object) => object,
3497 other => panic!("expected datetime object, got {other:?}"),
3498 }
3499 }
3500
3501 fn serial_for_date(year: i32, month: u32, day: u32) -> f64 {
3502 datenum_from_naive(midnight(NaiveDate::from_ymd_opt(year, month, day).unwrap()))
3503 }
3504
3505 #[test]
3506 fn datetime_descriptor_signatures_cover_constructor_and_methods() {
3507 let labels: Vec<&str> = DATETIME_DESCRIPTOR
3508 .signatures
3509 .iter()
3510 .map(|sig| sig.label)
3511 .collect();
3512 assert!(labels.contains(&"t = datetime()"));
3513 assert!(labels.contains(&"t = datetime(year, month, day, hour, minute, second)"));
3514 assert!(labels.contains(&"t = datetime(serialDateNumbers, \"ConvertFrom\", \"datenum\")"));
3515
3516 assert_eq!(DATETIME_YEAR_DESCRIPTOR.signatures[0].label, "X = year(t)");
3517 assert_eq!(
3518 DATETIME_SUBSREF_DESCRIPTOR.signatures[0].label,
3519 "out = datetime.subsref(obj, kind, payload)"
3520 );
3521 assert_eq!(
3522 DATETIME_BINARY_DESCRIPTOR.signatures[0].label,
3523 "out = datetime.op(lhs, rhs)"
3524 );
3525 }
3526
3527 #[test]
3528 fn datetime_builds_from_components() {
3529 let value = run_datetime(vec![Value::Num(2024.0), Value::Num(3.0), Value::Num(14.0)]);
3530 let object = as_datetime(value);
3531 assert_eq!(object.class_name, DATETIME_CLASS);
3532 assert_eq!(format_for_object(&object), DEFAULT_DATE_FORMAT);
3533 let serials = serial_tensor_for_object(&object).expect("serials");
3534 assert_eq!(serials.data.len(), 1);
3535 let year =
3536 futures::executor::block_on(year_builtin(Value::Object(object.clone()))).expect("year");
3537 assert_eq!(year, Value::Num(2024.0));
3538 }
3539
3540 #[test]
3541 fn datetime_builds_arrays_from_component_vectors() {
3542 let years = Value::Tensor(Tensor::new(vec![2024.0, 2025.0], vec![1, 2]).unwrap());
3543 let months = Value::Tensor(Tensor::new(vec![1.0, 6.0], vec![1, 2]).unwrap());
3544 let days = Value::Tensor(Tensor::new(vec![15.0, 20.0], vec![1, 2]).unwrap());
3545 let value = run_datetime(vec![years, months, days]);
3546 let object = as_datetime(value.clone());
3547 let serials = serial_tensor_for_object(&object).expect("serials");
3548 assert_eq!(serials.shape, vec![1, 2]);
3549 let rendered = datetime_display_text(&value)
3550 .expect("display")
3551 .expect("datetime text");
3552 assert!(rendered.contains("15-Jan-2024"));
3553 assert!(rendered.contains("20-Jun-2025"));
3554 }
3555
3556 #[test]
3557 fn datetime_parses_text_and_converts_to_strings() {
3558 let value = run_datetime(vec![Value::String("2024-03-14 09:26:53".to_string())]);
3559 let rendered = datetime_string_array(&value)
3560 .expect("string array")
3561 .expect("datetime strings");
3562 assert_eq!(rendered.data, vec!["14-Mar-2024 09:26:53".to_string()]);
3563 }
3564
3565 #[test]
3566 fn datetime_missing_serial_renders_as_nat() {
3567 let value = datetime_object_from_serial_tensor(
3568 Tensor::new(vec![f64::NAN], vec![1, 1]).unwrap(),
3569 DEFAULT_DATETIME_FORMAT,
3570 )
3571 .expect("datetime object");
3572 let rendered = datetime_string_array(&value)
3573 .expect("string array")
3574 .expect("datetime strings");
3575 assert_eq!(rendered.data, vec!["NaT".to_string()]);
3576 assert_eq!(
3577 datetime_display_text(&value).expect("display"),
3578 Some("NaT".to_string())
3579 );
3580 }
3581
3582 #[test]
3583 fn datetime_accepts_existing_datetime_input() {
3584 let value = run_datetime(vec![Value::String("2024-03-14".to_string())]);
3585 let converted = run_datetime(vec![
3586 value.clone(),
3587 Value::from("InputFormat"),
3588 Value::from("yyyy-MM-dd"),
3589 ]);
3590 assert_eq!(
3591 serials_from_datetime_value(&converted).unwrap().data,
3592 serials_from_datetime_value(&value).unwrap().data
3593 );
3594 }
3595
3596 #[test]
3597 fn datetime_parses_text_with_input_format() {
3598 let input = Value::StringArray(
3599 StringArray::new(
3600 vec!["2024/03/14".to_string(), "2024/03/15".to_string()],
3601 vec![2, 1],
3602 )
3603 .unwrap(),
3604 );
3605 let value = run_datetime(vec![
3606 input,
3607 Value::from("InputFormat"),
3608 Value::from("yyyy/MM/dd"),
3609 Value::from("Format"),
3610 Value::from("yyyy-MM-dd"),
3611 ]);
3612 let rendered = datetime_string_array(&value)
3613 .expect("string array")
3614 .expect("datetime strings");
3615 assert_eq!(
3616 rendered.data,
3617 vec!["2024-03-14".to_string(), "2024-03-15".to_string()]
3618 );
3619 }
3620
3621 #[test]
3622 fn dateshift_supports_start_of_week_and_month_end() {
3623 let input = run_datetime(vec![
3624 Value::StringArray(
3625 StringArray::new(
3626 vec!["2024-03-14".to_string(), "2024-03-18".to_string()],
3627 vec![2, 1],
3628 )
3629 .unwrap(),
3630 ),
3631 Value::from("Format"),
3632 Value::from("yyyy-MM-dd"),
3633 ]);
3634 let shifted = futures::executor::block_on(dateshift_builtin(
3635 input,
3636 Value::from("start"),
3637 Value::from("week"),
3638 Vec::new(),
3639 ))
3640 .expect("dateshift start week");
3641 let rendered = datetime_string_array(&shifted)
3642 .expect("string array")
3643 .expect("datetime strings");
3644 assert_eq!(
3645 rendered.data,
3646 vec!["2024-03-11".to_string(), "2024-03-18".to_string()]
3647 );
3648
3649 let month_end = futures::executor::block_on(dateshift_builtin(
3650 run_datetime(vec![
3651 Value::from("2024-02-10"),
3652 Value::from("Format"),
3653 Value::from("yyyy-MM-dd HH:mm:ss"),
3654 ]),
3655 Value::from("end"),
3656 Value::from("month"),
3657 Vec::new(),
3658 ))
3659 .expect("dateshift end month");
3660 let rendered = datetime_string_array(&month_end)
3661 .expect("string array")
3662 .expect("datetime strings");
3663 assert_eq!(rendered.data, vec!["2024-02-29 23:59:59".to_string()]);
3664 }
3665
3666 #[test]
3667 fn dateshift_rejects_unsupported_extra_arguments() {
3668 let input = run_datetime(vec![Value::from("2024-03-14")]);
3669 let err = futures::executor::block_on(dateshift_builtin(
3670 input.clone(),
3671 Value::from("dayofweek"),
3672 Value::from("monday"),
3673 vec![Value::from("extra")],
3674 ))
3675 .expect_err("dayofweek extra argument should fail");
3676 assert!(err.message().contains("does not accept extra arguments"));
3677
3678 let err = futures::executor::block_on(dateshift_builtin(
3679 input.clone(),
3680 Value::from("start"),
3681 Value::from("week"),
3682 vec![Value::from("monday"), Value::from("extra")],
3683 ))
3684 .expect_err("week second extra argument should fail");
3685 assert!(err.message().contains("at most one weekday argument"));
3686
3687 let err = futures::executor::block_on(dateshift_builtin(
3688 input,
3689 Value::from("start"),
3690 Value::from("month"),
3691 vec![Value::from("monday")],
3692 ))
3693 .expect_err("non-week extra argument should fail");
3694 assert!(err
3695 .message()
3696 .contains("extra arguments are only supported for week units"));
3697 }
3698
3699 #[test]
3700 fn datetime_supports_format_assignment() {
3701 let value = run_datetime(vec![Value::Num(2024.0), Value::Num(3.0), Value::Num(14.0)]);
3702 let updated = futures::executor::block_on(datetime_subsasgn(
3703 value,
3704 ".".to_string(),
3705 Value::String(FORMAT_FIELD.to_string()),
3706 Value::String("yyyy-MM-dd".to_string()),
3707 ))
3708 .expect("subsasgn");
3709 let rendered = datetime_display_text(&updated)
3710 .expect("display")
3711 .expect("datetime text");
3712 assert_eq!(rendered, "2024-03-14");
3713 }
3714
3715 #[test]
3716 fn datetime_supports_indexing_and_comparison() {
3717 let years = Value::Tensor(Tensor::new(vec![2024.0, 2025.0], vec![1, 2]).unwrap());
3718 let months = Value::Tensor(Tensor::new(vec![1.0, 6.0], vec![1, 2]).unwrap());
3719 let days = Value::Tensor(Tensor::new(vec![15.0, 20.0], vec![1, 2]).unwrap());
3720 let value = run_datetime(vec![years, months, days]);
3721 let payload =
3722 Value::Cell(runmat_builtins::CellArray::new(vec![Value::Num(2.0)], 1, 1).unwrap());
3723 let indexed =
3724 futures::executor::block_on(datetime_subsref(value.clone(), "()".to_string(), payload))
3725 .expect("subsref");
3726 let year = futures::executor::block_on(year_builtin(indexed)).expect("year");
3727 assert_eq!(year, Value::Num(2025.0));
3728
3729 let lhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(1.0)]);
3730 let rhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(2.0)]);
3731 let cmp = futures::executor::block_on(datetime_lt(lhs, rhs)).expect("lt");
3732 assert_eq!(cmp, Value::Num(1.0));
3733 }
3734
3735 #[test]
3736 fn datetime_and_duration_interoperate() {
3737 let lhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(1.0)]);
3738 let rhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(2.0)]);
3739 let delta = futures::executor::block_on(datetime_minus(rhs.clone(), lhs.clone()))
3740 .expect("datetime minus datetime");
3741 assert_eq!(delta, Value::Num(1.0));
3742
3743 let duration = crate::builtins::duration::duration_object_from_days_tensor(
3744 Tensor::new(vec![1.0], vec![1, 1]).unwrap(),
3745 crate::builtins::duration::DEFAULT_DURATION_FORMAT,
3746 )
3747 .expect("duration");
3748
3749 let round_trip = futures::executor::block_on(datetime_plus(lhs.clone(), duration.clone()))
3750 .expect("plus");
3751 let round_trip_text = datetime_display_text(&round_trip)
3752 .expect("datetime display")
3753 .expect("datetime text");
3754 assert_eq!(round_trip_text, "02-Jan-2024");
3755
3756 let restored =
3757 futures::executor::block_on(datetime_minus(rhs, duration)).expect("minus duration");
3758 let restored_text = datetime_display_text(&restored)
3759 .expect("datetime display")
3760 .expect("datetime text");
3761 assert_eq!(restored_text, "01-Jan-2024");
3762 }
3763
3764 #[test]
3765 fn legacy_date_conversion_and_query_helpers_work() {
3766 let serial = serial_for_date(2024, 3, 14);
3767 let date_vector =
3768 futures::executor::block_on(datevec_builtin(Value::Num(serial))).expect("datevec");
3769 let Value::Tensor(date_vector) = date_vector else {
3770 panic!("expected datevec tensor");
3771 };
3772 assert_eq!(date_vector.shape, vec![1, 6]);
3773 assert_eq!(&date_vector.data[..3], &[2024.0, 3.0, 14.0]);
3774
3775 let round_trip =
3776 futures::executor::block_on(datenum_builtin(vec![Value::Tensor(date_vector.clone())]))
3777 .expect("datenum");
3778 assert_eq!(round_trip, Value::Num(serial));
3779 let date_only_round_trip =
3780 futures::executor::block_on(datenum_builtin(vec![Value::Tensor(
3781 Tensor::new(vec![2024.0, 3.0, 14.0], vec![1, 3]).unwrap(),
3782 )]))
3783 .expect("datenum date vector");
3784 assert_eq!(date_only_round_trip, Value::Num(serial));
3785
3786 let text = futures::executor::block_on(datestr_builtin(
3787 Value::Num(serial),
3788 vec![Value::from("yyyy-MM-dd")],
3789 ))
3790 .expect("datestr");
3791 let Value::CharArray(text) = text else {
3792 panic!("expected datestr char array");
3793 };
3794 assert_eq!(text.data.iter().collect::<String>(), "2024-03-14");
3795 let text_from_datevec = futures::executor::block_on(datestr_builtin(
3796 Value::Tensor(Tensor::new(vec![2024.0, 3.0, 14.0], vec![1, 3]).unwrap()),
3797 vec![Value::from("yyyy-MM-dd")],
3798 ))
3799 .expect("datestr date vector");
3800 let Value::CharArray(text_from_datevec) = text_from_datevec else {
3801 panic!("expected datestr char array");
3802 };
3803 assert_eq!(
3804 text_from_datevec.data.iter().collect::<String>(),
3805 "2024-03-14"
3806 );
3807
3808 let weekday =
3809 futures::executor::block_on(weekday_builtin(Value::Num(serial))).expect("weekday");
3810 assert_eq!(weekday, Value::Num(5.0));
3811 assert_eq!(
3812 futures::executor::block_on(eomday_builtin(Value::Num(2024.0), Value::Num(2.0)))
3813 .expect("eomday"),
3814 Value::Num(29.0)
3815 );
3816 assert_eq!(
3817 futures::executor::block_on(etime_builtin(
3818 Value::Tensor(
3819 Tensor::new(vec![2024.0, 1.0, 2.0, 0.0, 0.0, 0.0], vec![1, 6]).unwrap(),
3820 ),
3821 Value::Tensor(
3822 Tensor::new(vec![2024.0, 1.0, 1.0, 0.0, 0.0, 0.0], vec![1, 6]).unwrap(),
3823 ),
3824 ))
3825 .expect("etime"),
3826 Value::Num(SECONDS_PER_DAY)
3827 );
3828 assert_eq!(
3829 futures::executor::block_on(isbetween_builtin(
3830 Value::Num(serial),
3831 Value::Num(serial - 1.0),
3832 Value::Num(serial + 1.0),
3833 ))
3834 .expect("isbetween"),
3835 Value::Num(1.0)
3836 );
3837 }
3838
3839 #[test]
3840 fn calendar_duration_helpers_and_datetime_arithmetic_work() {
3841 let one_month =
3842 futures::executor::block_on(calmonths_builtin(Value::Num(1.0))).expect("calmonths");
3843 assert_eq!(
3844 iscalendarduration_builtin(one_month.clone()).expect("predicate"),
3845 Value::Bool(true)
3846 );
3847 assert_eq!(
3848 futures::executor::block_on(calmonths_builtin(one_month.clone()))
3849 .expect("calmonths convert"),
3850 Value::Num(1.0)
3851 );
3852
3853 let jan31 = run_datetime(vec![
3854 Value::from("2024-01-31"),
3855 Value::from("Format"),
3856 Value::from("yyyy-MM-dd"),
3857 ]);
3858 let shifted = futures::executor::block_on(datetime_plus(jan31, one_month)).expect("plus");
3859 let rendered = datetime_string_array(&shifted)
3860 .expect("string array")
3861 .expect("datetime strings");
3862 assert_eq!(rendered.data, vec!["2024-02-29".to_string()]);
3863
3864 let duration = futures::executor::block_on(calendar_duration_builtin(vec![
3865 Value::Num(1.0),
3866 Value::Num(2.0),
3867 Value::Num(3.0),
3868 ]))
3869 .expect("calendarDuration");
3870 let (months, days) = calendar_duration_tensors_from_value(&duration).expect("components");
3871 assert_eq!(months.data, vec![14.0]);
3872 assert_eq!(days.data, vec![3.0]);
3873
3874 assert!(futures::executor::block_on(calyears_builtin(Value::Num(f64::MAX))).is_err());
3875 assert!(futures::executor::block_on(calendar_duration_builtin(vec![
3876 Value::Num(f64::MAX),
3877 Value::Num(f64::MAX),
3878 Value::Num(0.0),
3879 ]))
3880 .is_err());
3881 }
3882
3883 #[test]
3884 fn business_day_helpers_use_weekends_and_holidays() {
3885 let new_year = serial_for_date(2024, 1, 1);
3886 let friday = serial_for_date(2024, 1, 5);
3887 let saturday = serial_for_date(2024, 1, 6);
3888 let mask = futures::executor::block_on(isbusday_builtin(
3889 Value::Tensor(Tensor::new(vec![new_year, friday, saturday], vec![1, 3]).unwrap()),
3890 Vec::new(),
3891 ))
3892 .expect("isbusday");
3893 let Value::Tensor(mask) = mask else {
3894 panic!("expected isbusday tensor");
3895 };
3896 assert_eq!(mask.data, vec![0.0, 1.0, 0.0]);
3897
3898 assert_eq!(
3899 futures::executor::block_on(isbusday_builtin(
3900 Value::Num(friday),
3901 vec![Value::Num(friday)],
3902 ))
3903 .expect("custom holiday"),
3904 Value::Num(0.0)
3905 );
3906
3907 let business_days = futures::executor::block_on(busdays_builtin(
3908 Value::Num(friday),
3909 Value::Num(friday + 3.0),
3910 Vec::new(),
3911 ))
3912 .expect("busdays");
3913 let Value::Tensor(business_days) = business_days else {
3914 panic!("expected busdays tensor");
3915 };
3916 assert_eq!(business_days.data, vec![friday, friday + 3.0]);
3917
3918 assert_eq!(
3919 futures::executor::block_on(days252bus_builtin(
3920 Value::Num(friday),
3921 Value::Num(friday + 3.0),
3922 Vec::new(),
3923 ))
3924 .expect("days252bus"),
3925 Value::Num(2.0)
3926 );
3927 assert_eq!(
3928 futures::executor::block_on(daysdif_builtin(
3929 Value::Num(friday),
3930 Value::Num(friday + 3.0),
3931 Vec::new(),
3932 ))
3933 .expect("daysdif"),
3934 Value::Num(3.0)
3935 );
3936 assert_eq!(
3937 futures::executor::block_on(fbusdate_builtin(
3938 Value::Num(2024.0),
3939 Value::Num(1.0),
3940 Vec::new(),
3941 ))
3942 .expect("fbusdate"),
3943 Value::Num(serial_for_date(2024, 1, 2))
3944 );
3945 assert_eq!(
3946 futures::executor::block_on(lbusdate_builtin(
3947 Value::Num(2024.0),
3948 Value::Num(6.0),
3949 Vec::new(),
3950 ))
3951 .expect("lbusdate"),
3952 Value::Num(serial_for_date(2024, 6, 28))
3953 );
3954
3955 let holidays = futures::executor::block_on(holidays_builtin(vec![Value::Num(2024.0)]))
3956 .expect("holidays");
3957 let serials = serials_from_datetime_value(&holidays).expect("holiday serials");
3958 assert!(serials.data.contains(&serial_for_date(2024, 1, 1)));
3959 }
3960}