1use std::cell::Cell;
2use std::collections::HashMap;
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 SERIAL_FIELD: &str = "__serial";
20const FORMAT_FIELD: &str = "Format";
21const DEFAULT_DATE_FORMAT: &str = "dd-MMM-yyyy";
22const DEFAULT_DATETIME_FORMAT: &str = "dd-MMM-yyyy HH:mm:ss";
23const UNIX_DATENUM: f64 = 719_529.0;
24const SECONDS_PER_DAY: f64 = 86_400.0;
25
26thread_local! {
27 static DATETIME_CLASS_REGISTERED: Cell<bool> = const { Cell::new(false) };
28}
29
30const DATETIME_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
31 code: "RM.DATETIME.INVALID_ARGUMENT",
32 identifier: Some("RunMat:datetime:InvalidArgument"),
33 when: "Arguments or option grammar do not match supported datetime forms.",
34 message: "datetime: invalid argument",
35};
36const DATETIME_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
37 code: "RM.DATETIME.INVALID_INPUT",
38 identifier: Some("RunMat:datetime:InvalidInput"),
39 when: "Input values cannot be parsed/converted/broadcast to a valid datetime result.",
40 message: "datetime: invalid input",
41};
42const DATETIME_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
43 code: "RM.DATETIME.INTERNAL",
44 identifier: Some("RunMat:datetime:Internal"),
45 when: "Internal datetime state or indexing/evaluation failed unexpectedly.",
46 message: "datetime: internal operation failed",
47};
48const DATETIME_ERRORS: [BuiltinErrorDescriptor; 3] = [
49 DATETIME_ERROR_INVALID_ARGUMENT,
50 DATETIME_ERROR_INVALID_INPUT,
51 DATETIME_ERROR_INTERNAL,
52];
53
54const OUT_DATETIME: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
55 name: "t",
56 ty: BuiltinParamType::Any,
57 arity: BuiltinParamArity::Required,
58 default: None,
59 description: "Datetime object result.",
60}];
61const OUT_NUMERIC: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
62 name: "X",
63 ty: BuiltinParamType::Any,
64 arity: BuiltinParamArity::Required,
65 default: None,
66 description: "Numeric scalar/tensor result.",
67}];
68const OUT_ANY: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
69 name: "out",
70 ty: BuiltinParamType::Any,
71 arity: BuiltinParamArity::Required,
72 default: None,
73 description: "Method result.",
74}];
75const DATETIME_ARGS_ONLY: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
76 name: "args",
77 ty: BuiltinParamType::Any,
78 arity: BuiltinParamArity::Variadic,
79 default: None,
80 description: "Datetime constructor arguments.",
81}];
82const DATETIME_SINGLE_INPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
83 name: "value",
84 ty: BuiltinParamType::Any,
85 arity: BuiltinParamArity::Required,
86 default: None,
87 description: "Datetime input.",
88}];
89const DATETIME_BINARY_INPUTS: [BuiltinParamDescriptor; 2] = [
90 BuiltinParamDescriptor {
91 name: "lhs",
92 ty: BuiltinParamType::Any,
93 arity: BuiltinParamArity::Required,
94 default: None,
95 description: "Left datetime operand.",
96 },
97 BuiltinParamDescriptor {
98 name: "rhs",
99 ty: BuiltinParamType::Any,
100 arity: BuiltinParamArity::Required,
101 default: None,
102 description: "Right datetime/numeric/duration operand.",
103 },
104];
105const DATESHIFT_INPUTS: [BuiltinParamDescriptor; 4] = [
106 BuiltinParamDescriptor {
107 name: "t",
108 ty: BuiltinParamType::Any,
109 arity: BuiltinParamArity::Required,
110 default: None,
111 description: "Datetime input.",
112 },
113 BuiltinParamDescriptor {
114 name: "boundary",
115 ty: BuiltinParamType::StringScalar,
116 arity: BuiltinParamArity::Required,
117 default: None,
118 description: "Shift boundary: 'start', 'end', or 'nearest'.",
119 },
120 BuiltinParamDescriptor {
121 name: "unit",
122 ty: BuiltinParamType::StringScalar,
123 arity: BuiltinParamArity::Required,
124 default: None,
125 description: "Calendar/time unit.",
126 },
127 BuiltinParamDescriptor {
128 name: "weekdayOrOption",
129 ty: BuiltinParamType::Any,
130 arity: BuiltinParamArity::Optional,
131 default: None,
132 description: "Optional weekday for week-based shifts.",
133 },
134];
135const DATETIME_SUBSREF_INPUTS: [BuiltinParamDescriptor; 3] = [
136 BuiltinParamDescriptor {
137 name: "obj",
138 ty: BuiltinParamType::Any,
139 arity: BuiltinParamArity::Required,
140 default: None,
141 description: "Datetime receiver object.",
142 },
143 BuiltinParamDescriptor {
144 name: "kind",
145 ty: BuiltinParamType::StringScalar,
146 arity: BuiltinParamArity::Required,
147 default: None,
148 description: "Indexing kind token.",
149 },
150 BuiltinParamDescriptor {
151 name: "payload",
152 ty: BuiltinParamType::Any,
153 arity: BuiltinParamArity::Required,
154 default: None,
155 description: "Index/member payload.",
156 },
157];
158const DATETIME_SUBSASGN_INPUTS: [BuiltinParamDescriptor; 4] = [
159 BuiltinParamDescriptor {
160 name: "obj",
161 ty: BuiltinParamType::Any,
162 arity: BuiltinParamArity::Required,
163 default: None,
164 description: "Datetime receiver object.",
165 },
166 BuiltinParamDescriptor {
167 name: "kind",
168 ty: BuiltinParamType::StringScalar,
169 arity: BuiltinParamArity::Required,
170 default: None,
171 description: "Indexing kind token.",
172 },
173 BuiltinParamDescriptor {
174 name: "payload",
175 ty: BuiltinParamType::Any,
176 arity: BuiltinParamArity::Required,
177 default: None,
178 description: "Index/member payload.",
179 },
180 BuiltinParamDescriptor {
181 name: "rhs",
182 ty: BuiltinParamType::Any,
183 arity: BuiltinParamArity::Required,
184 default: None,
185 description: "Assigned value.",
186 },
187];
188
189const DATETIME_SIGNATURES: [BuiltinSignatureDescriptor; 11] = [
190 BuiltinSignatureDescriptor {
191 label: "t = datetime()",
192 inputs: &[],
193 outputs: &OUT_DATETIME,
194 },
195 BuiltinSignatureDescriptor {
196 label: "t = datetime(textOrArray)",
197 inputs: &[BuiltinParamDescriptor {
198 name: "textOrArray",
199 ty: BuiltinParamType::Any,
200 arity: BuiltinParamArity::Required,
201 default: None,
202 description: "String/char/date text input.",
203 }],
204 outputs: &OUT_DATETIME,
205 },
206 BuiltinSignatureDescriptor {
207 label: "t = datetime(serialDateNumbers)",
208 inputs: &[BuiltinParamDescriptor {
209 name: "serialDateNumbers",
210 ty: BuiltinParamType::NumericArray,
211 arity: BuiltinParamArity::Required,
212 default: None,
213 description: "Numeric serial date input.",
214 }],
215 outputs: &OUT_DATETIME,
216 },
217 BuiltinSignatureDescriptor {
218 label: "t = datetime(year, month, day)",
219 inputs: &[
220 BuiltinParamDescriptor {
221 name: "year",
222 ty: BuiltinParamType::NumericArray,
223 arity: BuiltinParamArity::Required,
224 default: None,
225 description: "Year component.",
226 },
227 BuiltinParamDescriptor {
228 name: "month",
229 ty: BuiltinParamType::NumericArray,
230 arity: BuiltinParamArity::Required,
231 default: None,
232 description: "Month component.",
233 },
234 BuiltinParamDescriptor {
235 name: "day",
236 ty: BuiltinParamType::NumericArray,
237 arity: BuiltinParamArity::Required,
238 default: None,
239 description: "Day component.",
240 },
241 ],
242 outputs: &OUT_DATETIME,
243 },
244 BuiltinSignatureDescriptor {
245 label: "t = datetime(year, month, day, hour)",
246 inputs: &[
247 BuiltinParamDescriptor {
248 name: "year",
249 ty: BuiltinParamType::NumericArray,
250 arity: BuiltinParamArity::Required,
251 default: None,
252 description: "Year component.",
253 },
254 BuiltinParamDescriptor {
255 name: "month",
256 ty: BuiltinParamType::NumericArray,
257 arity: BuiltinParamArity::Required,
258 default: None,
259 description: "Month component.",
260 },
261 BuiltinParamDescriptor {
262 name: "day",
263 ty: BuiltinParamType::NumericArray,
264 arity: BuiltinParamArity::Required,
265 default: None,
266 description: "Day component.",
267 },
268 BuiltinParamDescriptor {
269 name: "hour",
270 ty: BuiltinParamType::NumericArray,
271 arity: BuiltinParamArity::Required,
272 default: None,
273 description: "Hour component.",
274 },
275 ],
276 outputs: &OUT_DATETIME,
277 },
278 BuiltinSignatureDescriptor {
279 label: "t = datetime(year, month, day, hour, minute)",
280 inputs: &[
281 BuiltinParamDescriptor {
282 name: "year",
283 ty: BuiltinParamType::NumericArray,
284 arity: BuiltinParamArity::Required,
285 default: None,
286 description: "Year component.",
287 },
288 BuiltinParamDescriptor {
289 name: "month",
290 ty: BuiltinParamType::NumericArray,
291 arity: BuiltinParamArity::Required,
292 default: None,
293 description: "Month component.",
294 },
295 BuiltinParamDescriptor {
296 name: "day",
297 ty: BuiltinParamType::NumericArray,
298 arity: BuiltinParamArity::Required,
299 default: None,
300 description: "Day component.",
301 },
302 BuiltinParamDescriptor {
303 name: "hour",
304 ty: BuiltinParamType::NumericArray,
305 arity: BuiltinParamArity::Required,
306 default: None,
307 description: "Hour component.",
308 },
309 BuiltinParamDescriptor {
310 name: "minute",
311 ty: BuiltinParamType::NumericArray,
312 arity: BuiltinParamArity::Required,
313 default: None,
314 description: "Minute component.",
315 },
316 ],
317 outputs: &OUT_DATETIME,
318 },
319 BuiltinSignatureDescriptor {
320 label: "t = datetime(year, month, day, hour, minute, second)",
321 inputs: &[
322 BuiltinParamDescriptor {
323 name: "year",
324 ty: BuiltinParamType::NumericArray,
325 arity: BuiltinParamArity::Required,
326 default: None,
327 description: "Year component.",
328 },
329 BuiltinParamDescriptor {
330 name: "month",
331 ty: BuiltinParamType::NumericArray,
332 arity: BuiltinParamArity::Required,
333 default: None,
334 description: "Month component.",
335 },
336 BuiltinParamDescriptor {
337 name: "day",
338 ty: BuiltinParamType::NumericArray,
339 arity: BuiltinParamArity::Required,
340 default: None,
341 description: "Day component.",
342 },
343 BuiltinParamDescriptor {
344 name: "hour",
345 ty: BuiltinParamType::NumericArray,
346 arity: BuiltinParamArity::Required,
347 default: None,
348 description: "Hour component.",
349 },
350 BuiltinParamDescriptor {
351 name: "minute",
352 ty: BuiltinParamType::NumericArray,
353 arity: BuiltinParamArity::Required,
354 default: None,
355 description: "Minute component.",
356 },
357 BuiltinParamDescriptor {
358 name: "second",
359 ty: BuiltinParamType::NumericArray,
360 arity: BuiltinParamArity::Required,
361 default: None,
362 description: "Second component.",
363 },
364 ],
365 outputs: &OUT_DATETIME,
366 },
367 BuiltinSignatureDescriptor {
368 label: "t = datetime(serialDateNumbers, \"ConvertFrom\", \"datenum\")",
369 inputs: &[BuiltinParamDescriptor {
370 name: "args",
371 ty: BuiltinParamType::Any,
372 arity: BuiltinParamArity::Variadic,
373 default: None,
374 description: "Numeric serial input with ConvertFrom option.",
375 }],
376 outputs: &OUT_DATETIME,
377 },
378 BuiltinSignatureDescriptor {
379 label: "t = datetime(___, \"Format\", format)",
380 inputs: &DATETIME_ARGS_ONLY,
381 outputs: &OUT_DATETIME,
382 },
383 BuiltinSignatureDescriptor {
384 label: "t = datetime(textOrArray, \"InputFormat\", inputFormat)",
385 inputs: &DATETIME_ARGS_ONLY,
386 outputs: &OUT_DATETIME,
387 },
388 BuiltinSignatureDescriptor {
389 label: "t = datetime(___, Name, Value, ...)",
390 inputs: &DATETIME_ARGS_ONLY,
391 outputs: &OUT_DATETIME,
392 },
393];
394
395const DATETIME_YEAR_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
396 label: "X = year(t)",
397 inputs: &DATETIME_SINGLE_INPUT,
398 outputs: &OUT_NUMERIC,
399}];
400const DATETIME_MONTH_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
401 label: "X = month(t)",
402 inputs: &DATETIME_SINGLE_INPUT,
403 outputs: &OUT_NUMERIC,
404}];
405const DATETIME_DAY_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
406 label: "X = day(t)",
407 inputs: &DATETIME_SINGLE_INPUT,
408 outputs: &OUT_NUMERIC,
409}];
410const DATETIME_HOUR_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
411 label: "X = hour(t)",
412 inputs: &DATETIME_SINGLE_INPUT,
413 outputs: &OUT_NUMERIC,
414}];
415const DATETIME_MINUTE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
416 label: "X = minute(t)",
417 inputs: &DATETIME_SINGLE_INPUT,
418 outputs: &OUT_NUMERIC,
419}];
420const DATETIME_SECOND_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
421 label: "X = second(t)",
422 inputs: &DATETIME_SINGLE_INPUT,
423 outputs: &OUT_NUMERIC,
424}];
425const DATETIME_SUBSREF_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
426 label: "out = datetime.subsref(obj, kind, payload)",
427 inputs: &DATETIME_SUBSREF_INPUTS,
428 outputs: &OUT_ANY,
429}];
430const DATETIME_SUBSASGN_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
431 [BuiltinSignatureDescriptor {
432 label: "out = datetime.subsasgn(obj, kind, payload, rhs)",
433 inputs: &DATETIME_SUBSASGN_INPUTS,
434 outputs: &OUT_ANY,
435 }];
436const DATETIME_BINARY_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
437 label: "out = datetime.op(lhs, rhs)",
438 inputs: &DATETIME_BINARY_INPUTS,
439 outputs: &OUT_ANY,
440}];
441const DATESHIFT_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
442 BuiltinSignatureDescriptor {
443 label: "t2 = dateshift(t, boundary, unit)",
444 inputs: &DATESHIFT_INPUTS,
445 outputs: &OUT_DATETIME,
446 },
447 BuiltinSignatureDescriptor {
448 label: "t2 = dateshift(t, boundary, \"week\", weekday)",
449 inputs: &DATESHIFT_INPUTS,
450 outputs: &OUT_DATETIME,
451 },
452 BuiltinSignatureDescriptor {
453 label: "t2 = dateshift(t, \"dayofweek\", weekday)",
454 inputs: &DATESHIFT_INPUTS,
455 outputs: &OUT_DATETIME,
456 },
457];
458
459pub const DATETIME_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
460 signatures: &DATETIME_SIGNATURES,
461 output_mode: BuiltinOutputMode::Fixed,
462 completion_policy: BuiltinCompletionPolicy::Public,
463 errors: &DATETIME_ERRORS,
464};
465pub const DATETIME_YEAR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
466 signatures: &DATETIME_YEAR_SIGNATURES,
467 output_mode: BuiltinOutputMode::Fixed,
468 completion_policy: BuiltinCompletionPolicy::Public,
469 errors: &DATETIME_ERRORS,
470};
471pub const DATETIME_MONTH_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
472 signatures: &DATETIME_MONTH_SIGNATURES,
473 output_mode: BuiltinOutputMode::Fixed,
474 completion_policy: BuiltinCompletionPolicy::Public,
475 errors: &DATETIME_ERRORS,
476};
477pub const DATETIME_DAY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
478 signatures: &DATETIME_DAY_SIGNATURES,
479 output_mode: BuiltinOutputMode::Fixed,
480 completion_policy: BuiltinCompletionPolicy::Public,
481 errors: &DATETIME_ERRORS,
482};
483pub const DATETIME_HOUR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
484 signatures: &DATETIME_HOUR_SIGNATURES,
485 output_mode: BuiltinOutputMode::Fixed,
486 completion_policy: BuiltinCompletionPolicy::Public,
487 errors: &DATETIME_ERRORS,
488};
489pub const DATETIME_MINUTE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
490 signatures: &DATETIME_MINUTE_SIGNATURES,
491 output_mode: BuiltinOutputMode::Fixed,
492 completion_policy: BuiltinCompletionPolicy::Public,
493 errors: &DATETIME_ERRORS,
494};
495pub const DATETIME_SECOND_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
496 signatures: &DATETIME_SECOND_SIGNATURES,
497 output_mode: BuiltinOutputMode::Fixed,
498 completion_policy: BuiltinCompletionPolicy::Public,
499 errors: &DATETIME_ERRORS,
500};
501pub const DATETIME_SUBSREF_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
502 signatures: &DATETIME_SUBSREF_SIGNATURES,
503 output_mode: BuiltinOutputMode::Fixed,
504 completion_policy: BuiltinCompletionPolicy::MethodOnly,
505 errors: &DATETIME_ERRORS,
506};
507pub const DATETIME_SUBSASGN_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
508 signatures: &DATETIME_SUBSASGN_SIGNATURES,
509 output_mode: BuiltinOutputMode::Fixed,
510 completion_policy: BuiltinCompletionPolicy::MethodOnly,
511 errors: &DATETIME_ERRORS,
512};
513pub const DATETIME_BINARY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
514 signatures: &DATETIME_BINARY_SIGNATURES,
515 output_mode: BuiltinOutputMode::Fixed,
516 completion_policy: BuiltinCompletionPolicy::MethodOnly,
517 errors: &DATETIME_ERRORS,
518};
519pub const DATESHIFT_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
520 signatures: &DATESHIFT_SIGNATURES,
521 output_mode: BuiltinOutputMode::Fixed,
522 completion_policy: BuiltinCompletionPolicy::Public,
523 errors: &DATETIME_ERRORS,
524};
525
526fn datetime_error(message: impl Into<String>) -> RuntimeError {
527 build_runtime_error(message)
528 .with_builtin(BUILTIN_NAME)
529 .build()
530}
531
532fn ensure_datetime_class_registered() {
533 DATETIME_CLASS_REGISTERED.with(|registered| {
534 if registered.get() {
535 return;
536 }
537 let mut properties = HashMap::new();
538 properties.insert(
539 FORMAT_FIELD.to_string(),
540 PropertyDef {
541 name: FORMAT_FIELD.to_string(),
542 is_static: false,
543 is_constant: false,
544 is_dependent: false,
545 get_access: Access::Public,
546 set_access: Access::Public,
547 default_value: Some(Value::String(DEFAULT_DATETIME_FORMAT.to_string())),
548 },
549 );
550
551 let mut methods = HashMap::new();
552 for name in [
553 OBJECT_SUBSREF_METHOD,
554 OBJECT_SUBSASGN_METHOD,
555 "plus",
556 "minus",
557 "eq",
558 "ne",
559 "lt",
560 "le",
561 "gt",
562 "ge",
563 ] {
564 methods.insert(
565 name.to_string(),
566 MethodDef {
567 name: name.to_string(),
568 is_static: false,
569 is_abstract: false,
570 is_sealed: false,
571 access: Access::Public,
572 function_name: format!("{DATETIME_CLASS}.{name}"),
573 implicit_class_argument: None,
574 },
575 );
576 }
577
578 runmat_builtins::register_class(ClassDef {
579 name: DATETIME_CLASS.to_string(),
580 parent: None,
581 properties,
582 methods,
583 });
584 registered.set(true);
585 });
586}
587
588async fn gather_args(args: &[Value]) -> BuiltinResult<Vec<Value>> {
589 let mut out = Vec::with_capacity(args.len());
590 for arg in args {
591 out.push(
592 gather_if_needed_async(arg)
593 .await
594 .map_err(|err| datetime_error(format!("datetime: {}", err.message())))?,
595 );
596 }
597 Ok(out)
598}
599
600fn scalar_text(value: &Value, context: &str) -> BuiltinResult<String> {
601 match value {
602 Value::String(text) => Ok(text.clone()),
603 Value::StringArray(array) if array.data.len() == 1 => Ok(array.data[0].clone()),
604 Value::CharArray(array) if array.rows == 1 => Ok(array.data.iter().collect()),
605 _ => Err(datetime_error(format!(
606 "datetime: {context} must be a string scalar or character vector"
607 ))),
608 }
609}
610
611#[derive(Default)]
612struct DatetimeOptions {
613 format: Option<String>,
614 convert_from: Option<String>,
615 input_format: Option<String>,
616}
617
618fn parse_trailing_options(args: &[Value]) -> BuiltinResult<(usize, DatetimeOptions)> {
619 let mut positional_end = args.len();
620 let mut options = DatetimeOptions::default();
621
622 while positional_end >= 2 {
623 let name = match scalar_text(&args[positional_end - 2], "option name") {
624 Ok(text) => text,
625 Err(_) => break,
626 };
627 let lowered = name.trim().to_ascii_lowercase();
628 let value = scalar_text(&args[positional_end - 1], &format!("{name} option"))?;
629 match lowered.as_str() {
630 "format" => options.format = Some(value),
631 "convertfrom" => options.convert_from = Some(value),
632 "inputformat" => options.input_format = Some(value),
633 _ => break,
634 }
635 positional_end -= 2;
636 }
637
638 Ok((positional_end, options))
639}
640
641fn tensor_from_numeric(value: Value, context: &str) -> BuiltinResult<Tensor> {
642 tensor::value_into_tensor_for(context, value)
643 .map_err(|message| datetime_error(format!("datetime: {message}")))
644}
645
646fn serial_tensor_from_value(value: Value, context: &str) -> BuiltinResult<Tensor> {
647 let tensor = tensor_from_numeric(value, context)?;
648 Tensor::new(
649 tensor.data.clone(),
650 tensor::default_shape_for(&tensor.shape, tensor.data.len()),
651 )
652 .map_err(|err| datetime_error(format!("datetime: {err}")))
653}
654
655fn format_for_object(obj: &ObjectInstance) -> String {
656 match obj.properties.get(FORMAT_FIELD) {
657 Some(Value::String(text)) => text.clone(),
658 Some(Value::StringArray(array)) if array.data.len() == 1 => array.data[0].clone(),
659 Some(Value::CharArray(array)) if array.rows == 1 => array.data.iter().collect(),
660 _ => DEFAULT_DATETIME_FORMAT.to_string(),
661 }
662}
663
664fn serial_tensor_for_object(obj: &ObjectInstance) -> BuiltinResult<Tensor> {
665 match obj.properties.get(SERIAL_FIELD) {
666 Some(Value::Tensor(tensor)) => Ok(tensor.clone()),
667 Some(Value::Num(value)) => Tensor::new(vec![*value], vec![1, 1])
668 .map_err(|err| datetime_error(format!("datetime: {err}"))),
669 Some(other) => Err(datetime_error(format!(
670 "datetime: invalid internal serial storage {other:?}"
671 ))),
672 None => Err(datetime_error("datetime: missing internal serial storage")),
673 }
674}
675
676pub(crate) fn datetime_object_from_serial_tensor(
677 serials: Tensor,
678 format: impl Into<String>,
679) -> BuiltinResult<Value> {
680 ensure_datetime_class_registered();
681 let mut object = ObjectInstance::new(DATETIME_CLASS.to_string());
682 object
683 .properties
684 .insert(SERIAL_FIELD.to_string(), Value::Tensor(serials));
685 object
686 .properties
687 .insert(FORMAT_FIELD.to_string(), Value::String(format.into()));
688 Ok(Value::Object(object))
689}
690
691fn datetime_object_from_serials(
692 serials: Vec<f64>,
693 shape: Vec<usize>,
694 format: impl Into<String>,
695) -> BuiltinResult<Value> {
696 let tensor =
697 Tensor::new(serials, shape).map_err(|err| datetime_error(format!("datetime: {err}")))?;
698 datetime_object_from_serial_tensor(tensor, format)
699}
700
701fn format_token_to_strftime(format: &str) -> String {
702 let mut out = format.to_string();
703 for (src, dst) in [
704 ("yyyy", "%Y"),
705 ("MMM", "%b"),
706 ("MM", "%m"),
707 ("dd", "%d"),
708 ("HH", "%H"),
709 ("mm", "%M"),
710 ("ss", "%S"),
711 ] {
712 out = out.replace(src, dst);
713 }
714 out
715}
716
717pub(crate) fn datenum_from_naive(datetime: NaiveDateTime) -> f64 {
718 let base = NaiveDate::from_ymd_opt(1970, 1, 1)
719 .unwrap()
720 .and_hms_opt(0, 0, 0)
721 .unwrap();
722 let duration = datetime - base;
723 let seconds = duration.num_seconds();
724 let nanos = (duration - Duration::seconds(seconds))
725 .num_nanoseconds()
726 .unwrap_or(0);
727 let total_seconds = seconds as f64 + nanos as f64 / 1_000_000_000.0;
728 total_seconds / SECONDS_PER_DAY + UNIX_DATENUM
729}
730
731fn naive_from_datenum(serial: f64) -> BuiltinResult<NaiveDateTime> {
732 if !serial.is_finite() {
733 return Err(datetime_error(
734 "datetime: serial date numbers must be finite",
735 ));
736 }
737 let total_nanos = ((serial - UNIX_DATENUM) * SECONDS_PER_DAY * 1_000_000_000.0).round() as i128;
738 let seconds = total_nanos.div_euclid(1_000_000_000) as i64;
739 let nanos = total_nanos.rem_euclid(1_000_000_000) as i64;
740 let base = NaiveDate::from_ymd_opt(1970, 1, 1)
741 .unwrap()
742 .and_hms_opt(0, 0, 0)
743 .unwrap();
744 Ok(base + Duration::seconds(seconds) + Duration::nanoseconds(nanos))
745}
746
747fn format_serial(serial: f64, format: &str) -> BuiltinResult<String> {
748 let naive = naive_from_datenum(serial)?;
749 let chrono_format = format_token_to_strftime(format);
750 Ok(naive.format(&chrono_format).to_string())
751}
752
753fn parse_datetime_text(text: &str) -> Option<(NaiveDateTime, bool)> {
754 let trimmed = text.trim();
755 if trimmed.is_empty() {
756 return None;
757 }
758
759 if let Ok(value) = DateTime::parse_from_rfc3339(trimmed) {
760 return Some((value.with_timezone(&Local).naive_local(), true));
761 }
762
763 for (pattern, has_time) in [
764 ("%Y-%m-%d %H:%M:%S", true),
765 ("%Y-%m-%d", false),
766 ("%d-%b-%Y %H:%M:%S", true),
767 ("%d-%b-%Y", false),
768 ("%m/%d/%Y %H:%M:%S", true),
769 ("%m/%d/%Y", false),
770 ] {
771 if has_time {
772 if let Ok(value) = NaiveDateTime::parse_from_str(trimmed, pattern) {
773 return Some((value, true));
774 }
775 } else if let Ok(value) = NaiveDate::parse_from_str(trimmed, pattern) {
776 return Some((value.and_hms_opt(0, 0, 0).unwrap(), false));
777 }
778 }
779
780 None
781}
782
783fn parse_datetime_text_with_input_format(
784 text: &str,
785 input_format: Option<&str>,
786) -> Option<(NaiveDateTime, bool)> {
787 let trimmed = text.trim();
788 if trimmed.is_empty() {
789 return None;
790 }
791 let Some(input_format) = input_format else {
792 return parse_datetime_text(trimmed);
793 };
794 let chrono_format = format_token_to_strftime(input_format);
795 if let Ok(value) = NaiveDateTime::parse_from_str(trimmed, &chrono_format) {
796 return Some((value, true));
797 }
798 if let Ok(value) = NaiveDate::parse_from_str(trimmed, &chrono_format) {
799 return Some((value.and_hms_opt(0, 0, 0).unwrap(), false));
800 }
801 None
802}
803
804fn parse_text_input(
805 value: Value,
806 input_format: Option<&str>,
807) -> BuiltinResult<(Vec<f64>, Vec<usize>, String)> {
808 match value {
809 Value::String(text) => {
810 if text.trim().eq_ignore_ascii_case("now") {
811 let now = Local::now().naive_local();
812 return Ok((
813 vec![datenum_from_naive(now)],
814 vec![1, 1],
815 DEFAULT_DATETIME_FORMAT.to_string(),
816 ));
817 }
818 let (naive, has_time) = parse_datetime_text_with_input_format(&text, input_format)
819 .ok_or_else(|| {
820 datetime_error(format!("datetime: unable to parse date/time text '{text}'"))
821 })?;
822 Ok((
823 vec![datenum_from_naive(naive)],
824 vec![1, 1],
825 if has_time {
826 DEFAULT_DATETIME_FORMAT.to_string()
827 } else {
828 DEFAULT_DATE_FORMAT.to_string()
829 },
830 ))
831 }
832 Value::StringArray(array) => {
833 let mut serials = Vec::with_capacity(array.data.len());
834 let mut has_time = false;
835 for text in &array.data {
836 let (naive, parsed_has_time) =
837 parse_datetime_text_with_input_format(text, input_format).ok_or_else(|| {
838 datetime_error(format!("datetime: unable to parse date/time text '{text}'"))
839 })?;
840 serials.push(datenum_from_naive(naive));
841 has_time |= parsed_has_time;
842 }
843 Ok((
844 serials,
845 tensor::default_shape_for(&array.shape, array.data.len()),
846 if has_time {
847 DEFAULT_DATETIME_FORMAT.to_string()
848 } else {
849 DEFAULT_DATE_FORMAT.to_string()
850 },
851 ))
852 }
853 Value::CharArray(array) => {
854 let mut texts = Vec::with_capacity(array.rows);
855 for row in 0..array.rows {
856 let start = row * array.cols;
857 let end = start + array.cols;
858 texts.push(
859 array.data[start..end]
860 .iter()
861 .collect::<String>()
862 .trim_end()
863 .to_string(),
864 );
865 }
866 parse_text_input(
867 Value::StringArray(
868 StringArray::new(texts, vec![array.rows, 1])
869 .map_err(|err| datetime_error(format!("datetime: {err}")))?,
870 ),
871 input_format,
872 )
873 }
874 _ => Err(datetime_error(
875 "datetime: text input must be a string scalar, string array, or character array",
876 )),
877 }
878}
879
880fn round_component(value: f64, label: &str, min: i64, max: i64) -> BuiltinResult<i64> {
881 if !value.is_finite() {
882 return Err(datetime_error(format!(
883 "datetime: {label} values must be finite"
884 )));
885 }
886 let rounded = value.round();
887 if (rounded - value).abs() > 1e-9 {
888 return Err(datetime_error(format!(
889 "datetime: {label} values must be integers"
890 )));
891 }
892 let integer = rounded as i64;
893 if integer < min || integer > max {
894 return Err(datetime_error(format!(
895 "datetime: {label} values must be in the range [{min}, {max}]"
896 )));
897 }
898 Ok(integer)
899}
900
901fn naive_from_components(
902 year: f64,
903 month: f64,
904 day: f64,
905 hour: f64,
906 minute: f64,
907 second: f64,
908) -> BuiltinResult<NaiveDateTime> {
909 let year = round_component(year, "year", -262_000, 262_000)? as i32;
910 let month = round_component(month, "month", 1, 12)? as u32;
911 let day = round_component(day, "day", 1, 31)? as u32;
912 let hour = round_component(hour, "hour", 0, 23)? as u32;
913 let minute = round_component(minute, "minute", 0, 59)? as u32;
914 if !second.is_finite() {
915 return Err(datetime_error("datetime: second values must be finite"));
916 }
917 if !(0.0..60.0).contains(&second) {
918 return Err(datetime_error(
919 "datetime: second values must be in the range [0, 60)",
920 ));
921 }
922
923 let base_date = NaiveDate::from_ymd_opt(year, month, day)
924 .ok_or_else(|| datetime_error("datetime: invalid calendar date"))?;
925 let whole_second = second.floor();
926 let mut nanos = ((second - whole_second) * 1_000_000_000.0).round() as u32;
927 let mut secs = whole_second as u32;
928 if nanos == 1_000_000_000 {
929 secs += 1;
930 nanos = 0;
931 }
932 let time = base_date
933 .and_hms_nano_opt(hour, minute, secs, nanos)
934 .ok_or_else(|| datetime_error("datetime: invalid time components"))?;
935 Ok(time)
936}
937
938fn broadcast_component_data(
939 arrays: &[Tensor],
940 labels: &[&str],
941) -> BuiltinResult<(Vec<Vec<f64>>, Vec<usize>)> {
942 let mut target_shape = vec![1, 1];
943 let mut target_len = 1usize;
944
945 for array in arrays {
946 let len = array.data.len();
947 if len > 1 {
948 let shape = tensor::default_shape_for(&array.shape, len);
949 if target_len == 1 {
950 target_len = len;
951 target_shape = shape;
952 } else if len != target_len || shape != target_shape {
953 return Err(datetime_error(
954 "datetime: non-scalar component inputs must have matching sizes",
955 ));
956 }
957 }
958 }
959
960 let mut broadcasted = Vec::with_capacity(arrays.len());
961 for (idx, array) in arrays.iter().enumerate() {
962 if array.data.len() == 1 {
963 broadcasted.push(vec![array.data[0]; target_len]);
964 } else if array.data.len() == target_len {
965 broadcasted.push(array.data.clone());
966 } else {
967 return Err(datetime_error(format!(
968 "datetime: {} input size does not match the other components",
969 labels[idx]
970 )));
971 }
972 }
973
974 Ok((broadcasted, target_shape))
975}
976
977fn component_tensor(value: Value, context: &str) -> BuiltinResult<Tensor> {
978 let tensor = tensor_from_numeric(value, context)?;
979 Tensor::new(
980 tensor.data.clone(),
981 tensor::default_shape_for(&tensor.shape, tensor.data.len()),
982 )
983 .map_err(|err| datetime_error(format!("datetime: {err}")))
984}
985
986fn build_from_components(args: Vec<Value>, format: Option<String>) -> BuiltinResult<Value> {
987 let labels = ["year", "month", "day", "hour", "minute", "second"];
988 let input_count = args.len();
989 let mut arrays = Vec::with_capacity(args.len());
990 for (idx, arg) in args.into_iter().enumerate() {
991 arrays.push(component_tensor(arg, labels[idx])?);
992 }
993 while arrays.len() < 6 {
994 arrays.push(Tensor::new(vec![0.0], vec![1, 1]).unwrap());
995 }
996
997 let (broadcasted, shape) = broadcast_component_data(&arrays, &labels)?;
998 let len = broadcasted[0].len();
999 let mut serials = Vec::with_capacity(len);
1000 for idx in 0..len {
1001 let naive = naive_from_components(
1002 broadcasted[0][idx],
1003 broadcasted[1][idx],
1004 broadcasted[2][idx],
1005 broadcasted[3][idx],
1006 broadcasted[4][idx],
1007 broadcasted[5][idx],
1008 )?;
1009 serials.push(datenum_from_naive(naive));
1010 }
1011
1012 let default_format = if let Some(format) = format {
1013 format
1014 } else if input_count > 3 {
1015 DEFAULT_DATETIME_FORMAT.to_string()
1016 } else {
1017 DEFAULT_DATE_FORMAT.to_string()
1018 };
1019 datetime_object_from_serials(serials, shape, default_format)
1020}
1021
1022fn numeric_value_to_datetime(value: Value, format: Option<String>) -> BuiltinResult<Value> {
1023 let serials = serial_tensor_from_value(value, "datetime")?;
1024 datetime_object_from_serial_tensor(
1025 serials,
1026 format.unwrap_or_else(|| DEFAULT_DATETIME_FORMAT.to_string()),
1027 )
1028}
1029
1030pub fn is_datetime_object(value: &Value) -> bool {
1031 matches!(value, Value::Object(obj) if obj.is_class(DATETIME_CLASS))
1032}
1033
1034pub(crate) fn serials_from_datetime_value(value: &Value) -> BuiltinResult<Tensor> {
1035 match value {
1036 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => serial_tensor_for_object(obj),
1037 _ => Err(datetime_error("datetime: expected a datetime value")),
1038 }
1039}
1040
1041pub(crate) fn datetime_format_from_value(value: &Value) -> String {
1042 match value {
1043 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => format_for_object(obj),
1044 _ => DEFAULT_DATETIME_FORMAT.to_string(),
1045 }
1046}
1047
1048pub fn datetime_string_array(value: &Value) -> BuiltinResult<Option<StringArray>> {
1049 let Value::Object(obj) = value else {
1050 return Ok(None);
1051 };
1052 if !obj.is_class(DATETIME_CLASS) {
1053 return Ok(None);
1054 }
1055 let serials = serial_tensor_for_object(obj)?;
1056 let format = format_for_object(obj);
1057 let mut strings = Vec::with_capacity(serials.data.len());
1058 for serial in &serials.data {
1059 strings.push(format_serial(*serial, &format)?);
1060 }
1061 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
1062 let array = StringArray::new(strings, shape)
1063 .map_err(|err| datetime_error(format!("datetime: {err}")))?;
1064 Ok(Some(array))
1065}
1066
1067pub fn datetime_display_text(value: &Value) -> BuiltinResult<Option<String>> {
1068 let Some(array) = datetime_string_array(value)? else {
1069 return Ok(None);
1070 };
1071 if array.data.len() == 1 {
1072 return Ok(Some(array.data[0].clone()));
1073 }
1074
1075 let rows = array.rows;
1076 let cols = array.cols;
1077 let mut widths = vec![0usize; cols];
1078 for col in 0..cols {
1079 for row in 0..rows {
1080 let idx = row + col * rows;
1081 widths[col] = widths[col].max(array.data[idx].chars().count());
1082 }
1083 }
1084
1085 let mut lines = Vec::with_capacity(rows);
1086 for row in 0..rows {
1087 let mut line = String::new();
1088 for col in 0..cols {
1089 if col > 0 {
1090 line.push_str(" ");
1091 }
1092 let idx = row + col * rows;
1093 let text = &array.data[idx];
1094 line.push_str(text);
1095 let padding = widths[col].saturating_sub(text.chars().count());
1096 if padding > 0 {
1097 line.push_str(&" ".repeat(padding));
1098 }
1099 }
1100 lines.push(line);
1101 }
1102 Ok(Some(lines.join("\n")))
1103}
1104
1105pub fn datetime_summary(value: &Value) -> BuiltinResult<Option<String>> {
1106 let Value::Object(obj) = value else {
1107 return Ok(None);
1108 };
1109 if !obj.is_class(DATETIME_CLASS) {
1110 return Ok(None);
1111 }
1112 let serials = serial_tensor_for_object(obj)?;
1113 if serials.data.len() == 1 {
1114 return datetime_display_text(value);
1115 }
1116 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
1117 Ok(Some(format!(
1118 "[{} datetime]",
1119 shape
1120 .iter()
1121 .map(|dim| dim.to_string())
1122 .collect::<Vec<_>>()
1123 .join("x")
1124 )))
1125}
1126
1127fn component_tensor_from_datetime(
1128 value: &Value,
1129 label: &str,
1130 extractor: impl Fn(&NaiveDateTime) -> f64,
1131) -> BuiltinResult<Value> {
1132 let serials = serials_from_datetime_value(value)?;
1133 let mut out = Vec::with_capacity(serials.data.len());
1134 for serial in &serials.data {
1135 let naive = naive_from_datenum(*serial)?;
1136 out.push(extractor(&naive));
1137 }
1138 if out.len() == 1 {
1139 Ok(Value::Num(out[0]))
1140 } else {
1141 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
1142 let tensor =
1143 Tensor::new(out, shape).map_err(|err| datetime_error(format!("{label}: {err}")))?;
1144 Ok(Value::Tensor(tensor))
1145 }
1146}
1147
1148fn tensor_or_scalar(data: Vec<f64>, shape: Vec<usize>) -> BuiltinResult<Value> {
1149 if data.len() == 1 {
1150 Ok(Value::Num(data[0]))
1151 } else {
1152 Ok(Value::Tensor(Tensor::new(data, shape).map_err(|err| {
1153 datetime_error(format!("datetime: {err}"))
1154 })?))
1155 }
1156}
1157
1158async fn datetime_indexing(obj: Value, payload: Value) -> BuiltinResult<Value> {
1159 let Value::Object(object) = obj else {
1160 return Err(datetime_error(
1161 "datetime.subsref: receiver must be a datetime object",
1162 ));
1163 };
1164 let format = format_for_object(&object);
1165 let serials = serial_tensor_for_object(&object)?;
1166
1167 let Value::Cell(cell) = payload else {
1168 return Err(datetime_error(
1169 "datetime.subsref: indexing payload must be a cell array",
1170 ));
1171 };
1172 if cell.data.is_empty() {
1173 return datetime_object_from_serial_tensor(serials, format);
1174 }
1175 if cell.data.len() != 1 {
1176 return Err(datetime_error(
1177 "datetime.subsref: only linear datetime indexing is currently supported",
1178 ));
1179 }
1180 let selector = cell.data[0].clone();
1181 let selector = match selector {
1182 Value::Tensor(tensor) => tensor,
1183 Value::Num(value) => Tensor::new(vec![value], vec![1, 1])
1184 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1185 Value::Int(value) => Tensor::new(vec![value.to_f64()], vec![1, 1])
1186 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1187 Value::LogicalArray(logical) => tensor::logical_to_tensor(&logical)
1188 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1189 other => {
1190 return Err(datetime_error(format!(
1191 "datetime.subsref: unsupported index value {other:?}"
1192 )))
1193 }
1194 };
1195 let indexed = crate::perform_indexing(&Value::Tensor(serials), &selector.data)
1196 .await
1197 .map_err(|err| datetime_error(format!("datetime.subsref: {}", err.message())))?;
1198 let indexed_serials = match indexed {
1199 Value::Num(value) => Tensor::new(vec![value], vec![1, 1])
1200 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1201 Value::Tensor(tensor) => tensor,
1202 other => {
1203 return Err(datetime_error(format!(
1204 "datetime.subsref: unexpected indexing result {other:?}"
1205 )))
1206 }
1207 };
1208 datetime_object_from_serial_tensor(indexed_serials, format)
1209}
1210
1211#[runmat_macros::runtime_builtin(
1212 name = "datetime",
1213 descriptor(crate::builtins::datetime::DATETIME_DESCRIPTOR),
1214 builtin_path = "crate::builtins::datetime",
1215 category = "datetime",
1216 summary = "Create datetime arrays from text, components, or serial date numbers.",
1217 keywords = "datetime,date,time,datenum,Format",
1218 related = "year,month,day,hour,minute,second,string,char,disp",
1219 examples = "t = datetime(2024, 4, 9, 13, 30, 0);"
1220)]
1221async fn datetime_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
1222 ensure_datetime_class_registered();
1223 let args = gather_args(&args).await?;
1224 let (positional_end, options) = parse_trailing_options(&args)?;
1225 let positional = args[..positional_end].to_vec();
1226
1227 if let Some(convert_from) = options.convert_from {
1228 if !convert_from.eq_ignore_ascii_case("datenum") {
1229 return Err(datetime_error(format!(
1230 "datetime: unsupported ConvertFrom value '{convert_from}'"
1231 )));
1232 }
1233 if positional.len() != 1 {
1234 return Err(datetime_error(
1235 "datetime: ConvertFrom='datenum' expects exactly one numeric input",
1236 ));
1237 }
1238 return numeric_value_to_datetime(positional[0].clone(), options.format);
1239 }
1240
1241 match positional.len() {
1242 0 => {
1243 let now = Local::now().naive_local();
1244 datetime_object_from_serials(
1245 vec![datenum_from_naive(now)],
1246 vec![1, 1],
1247 options
1248 .format
1249 .unwrap_or_else(|| DEFAULT_DATETIME_FORMAT.to_string()),
1250 )
1251 }
1252 1 => match &positional[0] {
1253 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => {
1254 let serials = serials_from_datetime_value(&positional[0])?;
1255 let format = options
1256 .format
1257 .unwrap_or_else(|| datetime_format_from_value(&positional[0]));
1258 datetime_object_from_serial_tensor(serials, format)
1259 }
1260 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => {
1261 let (serials, shape, inferred_format) =
1262 parse_text_input(positional[0].clone(), options.input_format.as_deref())?;
1263 datetime_object_from_serials(
1264 serials,
1265 shape,
1266 options.format.unwrap_or(inferred_format),
1267 )
1268 }
1269 _ => numeric_value_to_datetime(positional[0].clone(), options.format),
1270 },
1271 3..=6 => build_from_components(positional, options.format),
1272 _ => Err(datetime_error(
1273 "datetime: unsupported argument pattern; use text, serial dates, or Y/M/D component inputs",
1274 )),
1275 }
1276}
1277
1278#[runmat_macros::runtime_builtin(
1279 name = "year",
1280 descriptor(crate::builtins::datetime::DATETIME_YEAR_DESCRIPTOR),
1281 builtin_path = "crate::builtins::datetime",
1282 category = "datetime",
1283 summary = "Extract calendar year components from datetime values.",
1284 keywords = "year,datetime,date component"
1285)]
1286async fn year_builtin(value: Value) -> crate::BuiltinResult<Value> {
1287 component_tensor_from_datetime(&value, "year", |naive| naive.year() as f64)
1288}
1289
1290#[runmat_macros::runtime_builtin(
1291 name = "month",
1292 descriptor(crate::builtins::datetime::DATETIME_MONTH_DESCRIPTOR),
1293 builtin_path = "crate::builtins::datetime",
1294 category = "datetime",
1295 summary = "Extract month numbers from datetime arrays.",
1296 keywords = "month,datetime,date component"
1297)]
1298async fn month_builtin(value: Value) -> crate::BuiltinResult<Value> {
1299 component_tensor_from_datetime(&value, "month", |naive| naive.month() as f64)
1300}
1301
1302#[runmat_macros::runtime_builtin(
1303 name = "day",
1304 descriptor(crate::builtins::datetime::DATETIME_DAY_DESCRIPTOR),
1305 builtin_path = "crate::builtins::datetime",
1306 category = "datetime",
1307 summary = "Extract day-of-month numbers from datetime values.",
1308 keywords = "day,datetime,date component"
1309)]
1310async fn day_builtin(value: Value) -> crate::BuiltinResult<Value> {
1311 component_tensor_from_datetime(&value, "day", |naive| naive.day() as f64)
1312}
1313
1314#[runmat_macros::runtime_builtin(
1315 name = "hour",
1316 descriptor(crate::builtins::datetime::DATETIME_HOUR_DESCRIPTOR),
1317 builtin_path = "crate::builtins::datetime",
1318 category = "datetime",
1319 summary = "Extract hour components from datetime values.",
1320 keywords = "hour,datetime,time component"
1321)]
1322async fn hour_builtin(value: Value) -> crate::BuiltinResult<Value> {
1323 component_tensor_from_datetime(&value, "hour", |naive| naive.hour() as f64)
1324}
1325
1326#[runmat_macros::runtime_builtin(
1327 name = "minute",
1328 descriptor(crate::builtins::datetime::DATETIME_MINUTE_DESCRIPTOR),
1329 builtin_path = "crate::builtins::datetime",
1330 category = "datetime",
1331 summary = "Extract minute numbers from datetime arrays.",
1332 keywords = "minute,datetime,time component"
1333)]
1334async fn minute_builtin(value: Value) -> crate::BuiltinResult<Value> {
1335 component_tensor_from_datetime(&value, "minute", |naive| naive.minute() as f64)
1336}
1337
1338#[runmat_macros::runtime_builtin(
1339 name = "second",
1340 descriptor(crate::builtins::datetime::DATETIME_SECOND_DESCRIPTOR),
1341 builtin_path = "crate::builtins::datetime",
1342 category = "datetime",
1343 summary = "Extract second components from datetime values.",
1344 keywords = "second,datetime,time component"
1345)]
1346async fn second_builtin(value: Value) -> crate::BuiltinResult<Value> {
1347 component_tensor_from_datetime(&value, "second", |naive| {
1348 naive.second() as f64 + f64::from(naive.nanosecond()) / 1_000_000_000.0
1349 })
1350}
1351
1352#[runmat_macros::runtime_builtin(
1353 name = "datetime.subsref",
1354 descriptor(crate::builtins::datetime::DATETIME_SUBSREF_DESCRIPTOR),
1355 builtin_path = "crate::builtins::datetime"
1356)]
1357async fn datetime_subsref(obj: Value, kind: String, payload: Value) -> crate::BuiltinResult<Value> {
1358 match kind.as_str() {
1359 OBJECT_INDEX_PAREN => datetime_indexing(obj, payload).await,
1360 OBJECT_INDEX_MEMBER => {
1361 let Value::Object(object) = obj else {
1362 return Err(datetime_error(
1363 "datetime.subsref: receiver must be a datetime object",
1364 ));
1365 };
1366 let field = scalar_text(&payload, "field selector")?;
1367 match field.as_str() {
1368 FORMAT_FIELD => Ok(Value::String(format_for_object(&object))),
1369 _ => Err(datetime_error(format!(
1370 "datetime.subsref: unsupported datetime property '{field}'"
1371 ))),
1372 }
1373 }
1374 other => Err(datetime_error(format!(
1375 "datetime.subsref: unsupported indexing kind '{other}'"
1376 ))),
1377 }
1378}
1379
1380#[runmat_macros::runtime_builtin(
1381 name = "datetime.subsasgn",
1382 descriptor(crate::builtins::datetime::DATETIME_SUBSASGN_DESCRIPTOR),
1383 builtin_path = "crate::builtins::datetime"
1384)]
1385async fn datetime_subsasgn(
1386 obj: Value,
1387 kind: String,
1388 payload: Value,
1389 rhs: Value,
1390) -> crate::BuiltinResult<Value> {
1391 let Value::Object(mut object) = obj else {
1392 return Err(datetime_error(
1393 "datetime.subsasgn: receiver must be a datetime object",
1394 ));
1395 };
1396 match kind.as_str() {
1397 OBJECT_INDEX_MEMBER => {
1398 let field = scalar_text(&payload, "field selector")?;
1399 match field.as_str() {
1400 FORMAT_FIELD => {
1401 let text = scalar_text(&rhs, "Format value")?;
1402 object
1403 .properties
1404 .insert(FORMAT_FIELD.to_string(), Value::String(text));
1405 Ok(Value::Object(object))
1406 }
1407 _ => Err(datetime_error(format!(
1408 "datetime.subsasgn: unsupported datetime property '{field}'"
1409 ))),
1410 }
1411 }
1412 _ => Err(datetime_error(format!(
1413 "datetime.subsasgn: unsupported indexing kind '{kind}'"
1414 ))),
1415 }
1416}
1417
1418fn datetime_binary_serials(
1419 lhs: Value,
1420 rhs: Value,
1421 context: &str,
1422) -> BuiltinResult<(Tensor, Tensor, Vec<usize>, String)> {
1423 let lhs_serials = serials_from_datetime_value(&lhs)?;
1424 let rhs_serials = match &rhs {
1425 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => serial_tensor_for_object(obj)?,
1426 _ => serial_tensor_from_value(rhs, context)?,
1427 };
1428 let (left, right, shape) =
1429 tensor::binary_numeric_tensors(&lhs_serials, &rhs_serials, context, BUILTIN_NAME)?;
1430 let left_tensor = Tensor::new(left, shape.clone())
1431 .map_err(|err| datetime_error(format!("{context}: {err}")))?;
1432 let right_tensor = Tensor::new(right, shape.clone())
1433 .map_err(|err| datetime_error(format!("{context}: {err}")))?;
1434 Ok((
1435 left_tensor,
1436 right_tensor,
1437 shape,
1438 datetime_format_from_value(&lhs),
1439 ))
1440}
1441
1442fn compare_datetime(
1443 lhs: Value,
1444 rhs: Value,
1445 op: &str,
1446 cmp: impl Fn(f64, f64) -> bool,
1447) -> BuiltinResult<Value> {
1448 let (left, right, shape, _) = datetime_binary_serials(lhs, rhs, op)?;
1449 let out = left
1450 .data
1451 .iter()
1452 .zip(right.data.iter())
1453 .map(|(a, b)| if cmp(*a, *b) { 1.0 } else { 0.0 })
1454 .collect::<Vec<_>>();
1455 tensor_or_scalar(out, shape)
1456}
1457
1458#[runmat_macros::runtime_builtin(
1459 name = "datetime.eq",
1460 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1461 builtin_path = "crate::builtins::datetime"
1462)]
1463async fn datetime_eq(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1464 compare_datetime(lhs, rhs, "eq", |a, b| (a - b).abs() <= 1e-12)
1465}
1466
1467#[runmat_macros::runtime_builtin(
1468 name = "datetime.ne",
1469 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1470 builtin_path = "crate::builtins::datetime"
1471)]
1472async fn datetime_ne(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1473 compare_datetime(lhs, rhs, "ne", |a, b| (a - b).abs() > 1e-12)
1474}
1475
1476#[runmat_macros::runtime_builtin(
1477 name = "datetime.lt",
1478 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1479 builtin_path = "crate::builtins::datetime"
1480)]
1481async fn datetime_lt(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1482 compare_datetime(lhs, rhs, "lt", |a, b| a < b)
1483}
1484
1485#[runmat_macros::runtime_builtin(
1486 name = "datetime.le",
1487 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1488 builtin_path = "crate::builtins::datetime"
1489)]
1490async fn datetime_le(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1491 compare_datetime(lhs, rhs, "le", |a, b| a <= b)
1492}
1493
1494#[runmat_macros::runtime_builtin(
1495 name = "datetime.gt",
1496 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1497 builtin_path = "crate::builtins::datetime"
1498)]
1499async fn datetime_gt(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1500 compare_datetime(lhs, rhs, "gt", |a, b| a > b)
1501}
1502
1503#[runmat_macros::runtime_builtin(
1504 name = "datetime.ge",
1505 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1506 builtin_path = "crate::builtins::datetime"
1507)]
1508async fn datetime_ge(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1509 compare_datetime(lhs, rhs, "ge", |a, b| a >= b)
1510}
1511
1512#[runmat_macros::runtime_builtin(
1513 name = "datetime.plus",
1514 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1515 builtin_path = "crate::builtins::datetime"
1516)]
1517async fn datetime_plus(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1518 let lhs_serials = serials_from_datetime_value(&lhs)?;
1519 let rhs_numeric = if crate::builtins::duration::is_duration_object(&rhs) {
1520 crate::builtins::duration::duration_tensor_from_duration_value(&rhs)?
1521 } else {
1522 serial_tensor_from_value(rhs, "plus")?
1523 };
1524 let (left, right, shape) =
1525 tensor::binary_numeric_tensors(&lhs_serials, &rhs_numeric, "plus", BUILTIN_NAME)?;
1526 let serials = left
1527 .iter()
1528 .zip(right.iter())
1529 .map(|(a, b)| a + b)
1530 .collect::<Vec<_>>();
1531 datetime_object_from_serials(serials, shape, datetime_format_from_value(&lhs))
1532}
1533
1534#[runmat_macros::runtime_builtin(
1535 name = "datetime.minus",
1536 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1537 builtin_path = "crate::builtins::datetime"
1538)]
1539async fn datetime_minus(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1540 let lhs_serials = serials_from_datetime_value(&lhs)?;
1541 match &rhs {
1542 _ if crate::builtins::duration::is_duration_object(&rhs) => {
1543 let rhs_days = crate::builtins::duration::duration_tensor_from_duration_value(&rhs)?;
1544 let (left, right, shape) =
1545 tensor::binary_numeric_tensors(&lhs_serials, &rhs_days, "minus", BUILTIN_NAME)?;
1546 let serials = left
1547 .iter()
1548 .zip(right.iter())
1549 .map(|(a, b)| a - b)
1550 .collect::<Vec<_>>();
1551 datetime_object_from_serials(serials, shape, datetime_format_from_value(&lhs))
1552 }
1553 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => {
1554 let rhs_serials = serial_tensor_for_object(obj)?;
1555 let (left, right, shape) =
1556 tensor::binary_numeric_tensors(&lhs_serials, &rhs_serials, "minus", BUILTIN_NAME)?;
1557 let deltas = left
1558 .iter()
1559 .zip(right.iter())
1560 .map(|(a, b)| a - b)
1561 .collect::<Vec<_>>();
1562 tensor_or_scalar(deltas, shape)
1563 }
1564 _ => {
1565 let rhs_numeric = serial_tensor_from_value(rhs, "minus")?;
1566 let (left, right, shape) =
1567 tensor::binary_numeric_tensors(&lhs_serials, &rhs_numeric, "minus", BUILTIN_NAME)?;
1568 let serials = left
1569 .iter()
1570 .zip(right.iter())
1571 .map(|(a, b)| a - b)
1572 .collect::<Vec<_>>();
1573 datetime_object_from_serials(serials, shape, datetime_format_from_value(&lhs))
1574 }
1575 }
1576}
1577
1578#[derive(Clone, Copy, PartialEq, Eq)]
1579enum DateShiftBoundary {
1580 Start,
1581 End,
1582 Nearest,
1583 DayOfWeek,
1584}
1585
1586impl DateShiftBoundary {
1587 fn parse(value: &Value) -> BuiltinResult<Self> {
1588 let text = scalar_text(value, "dateshift boundary")?;
1589 match text.trim().to_ascii_lowercase().as_str() {
1590 "start" => Ok(Self::Start),
1591 "end" => Ok(Self::End),
1592 "nearest" => Ok(Self::Nearest),
1593 "dayofweek" => Ok(Self::DayOfWeek),
1594 other => Err(datetime_error(format!(
1595 "dateshift: unsupported boundary '{other}'"
1596 ))),
1597 }
1598 }
1599}
1600
1601#[derive(Clone, Copy)]
1602enum DateShiftUnit {
1603 Year,
1604 Quarter,
1605 Month,
1606 Week,
1607 Day,
1608 Hour,
1609 Minute,
1610 Second,
1611}
1612
1613impl DateShiftUnit {
1614 fn parse(value: &Value) -> BuiltinResult<Self> {
1615 let text = scalar_text(value, "dateshift unit")?;
1616 match text.trim().to_ascii_lowercase().as_str() {
1617 "year" | "years" => Ok(Self::Year),
1618 "quarter" | "quarters" => Ok(Self::Quarter),
1619 "month" | "months" => Ok(Self::Month),
1620 "week" | "weeks" => Ok(Self::Week),
1621 "day" | "days" => Ok(Self::Day),
1622 "hour" | "hours" => Ok(Self::Hour),
1623 "minute" | "minutes" => Ok(Self::Minute),
1624 "second" | "seconds" => Ok(Self::Second),
1625 other => Err(datetime_error(format!(
1626 "dateshift: unsupported unit '{other}'"
1627 ))),
1628 }
1629 }
1630}
1631
1632fn parse_weekday(value: &Value) -> BuiltinResult<Weekday> {
1633 match value {
1634 Value::Num(n) if n.is_finite() && (*n - n.round()).abs() <= f64::EPSILON => {
1635 weekday_from_matlab_index(n.round() as i64)
1636 }
1637 Value::Int(i) => weekday_from_matlab_index(i.to_i64()),
1638 _ => {
1639 let text = scalar_text(value, "weekday")?;
1640 match text.trim().to_ascii_lowercase().as_str() {
1641 "sun" | "sunday" => Ok(Weekday::Sun),
1642 "mon" | "monday" => Ok(Weekday::Mon),
1643 "tue" | "tues" | "tuesday" => Ok(Weekday::Tue),
1644 "wed" | "wednesday" => Ok(Weekday::Wed),
1645 "thu" | "thur" | "thurs" | "thursday" => Ok(Weekday::Thu),
1646 "fri" | "friday" => Ok(Weekday::Fri),
1647 "sat" | "saturday" => Ok(Weekday::Sat),
1648 other => Err(datetime_error(format!(
1649 "dateshift: unsupported weekday '{other}'"
1650 ))),
1651 }
1652 }
1653 }
1654}
1655
1656fn weekday_from_matlab_index(index: i64) -> BuiltinResult<Weekday> {
1657 match index {
1658 1 => Ok(Weekday::Sun),
1659 2 => Ok(Weekday::Mon),
1660 3 => Ok(Weekday::Tue),
1661 4 => Ok(Weekday::Wed),
1662 5 => Ok(Weekday::Thu),
1663 6 => Ok(Weekday::Fri),
1664 7 => Ok(Weekday::Sat),
1665 _ => Err(datetime_error(
1666 "dateshift: numeric weekdays must be in the range 1..7",
1667 )),
1668 }
1669}
1670
1671fn midnight(date: NaiveDate) -> NaiveDateTime {
1672 date.and_hms_opt(0, 0, 0).unwrap()
1673}
1674
1675fn start_of_week(value: NaiveDateTime, week_start: Weekday) -> NaiveDateTime {
1676 let current = value.weekday().num_days_from_monday() as i64;
1677 let start = week_start.num_days_from_monday() as i64;
1678 let delta = (current - start).rem_euclid(7);
1679 midnight(value.date() - Duration::days(delta))
1680}
1681
1682fn start_of_unit(value: NaiveDateTime, unit: DateShiftUnit, week_start: Weekday) -> NaiveDateTime {
1683 match unit {
1684 DateShiftUnit::Year => midnight(NaiveDate::from_ymd_opt(value.year(), 1, 1).unwrap()),
1685 DateShiftUnit::Quarter => {
1686 let month = ((value.month() - 1) / 3) * 3 + 1;
1687 midnight(NaiveDate::from_ymd_opt(value.year(), month, 1).unwrap())
1688 }
1689 DateShiftUnit::Month => {
1690 midnight(NaiveDate::from_ymd_opt(value.year(), value.month(), 1).unwrap())
1691 }
1692 DateShiftUnit::Week => start_of_week(value, week_start),
1693 DateShiftUnit::Day => midnight(value.date()),
1694 DateShiftUnit::Hour => value
1695 .date()
1696 .and_hms_nano_opt(value.hour(), 0, 0, 0)
1697 .unwrap(),
1698 DateShiftUnit::Minute => value
1699 .date()
1700 .and_hms_nano_opt(value.hour(), value.minute(), 0, 0)
1701 .unwrap(),
1702 DateShiftUnit::Second => value
1703 .date()
1704 .and_hms_nano_opt(value.hour(), value.minute(), value.second(), 0)
1705 .unwrap(),
1706 }
1707}
1708
1709fn add_months(year: i32, month: u32, delta: u32) -> (i32, u32) {
1710 let zero_based = year as i64 * 12 + i64::from(month - 1) + i64::from(delta);
1711 let out_year = zero_based.div_euclid(12) as i32;
1712 let out_month = zero_based.rem_euclid(12) as u32 + 1;
1713 (out_year, out_month)
1714}
1715
1716fn next_unit_start(start: NaiveDateTime, unit: DateShiftUnit) -> NaiveDateTime {
1717 match unit {
1718 DateShiftUnit::Year => midnight(NaiveDate::from_ymd_opt(start.year() + 1, 1, 1).unwrap()),
1719 DateShiftUnit::Quarter => {
1720 let (year, month) = add_months(start.year(), start.month(), 3);
1721 midnight(NaiveDate::from_ymd_opt(year, month, 1).unwrap())
1722 }
1723 DateShiftUnit::Month => {
1724 let (year, month) = add_months(start.year(), start.month(), 1);
1725 midnight(NaiveDate::from_ymd_opt(year, month, 1).unwrap())
1726 }
1727 DateShiftUnit::Week => start + Duration::days(7),
1728 DateShiftUnit::Day => start + Duration::days(1),
1729 DateShiftUnit::Hour => start + Duration::hours(1),
1730 DateShiftUnit::Minute => start + Duration::minutes(1),
1731 DateShiftUnit::Second => start + Duration::seconds(1),
1732 }
1733}
1734
1735fn shift_naive_datetime(
1736 value: NaiveDateTime,
1737 boundary: DateShiftBoundary,
1738 unit: DateShiftUnit,
1739 week_start: Weekday,
1740) -> NaiveDateTime {
1741 let start = start_of_unit(value, unit, week_start);
1742 match boundary {
1743 DateShiftBoundary::Start => start,
1744 DateShiftBoundary::End => next_unit_start(start, unit) - Duration::milliseconds(1),
1745 DateShiftBoundary::Nearest => {
1746 let next = next_unit_start(start, unit);
1747 if value - start <= next - value {
1748 start
1749 } else {
1750 next
1751 }
1752 }
1753 DateShiftBoundary::DayOfWeek => value,
1754 }
1755}
1756
1757fn shift_to_dayofweek(value: NaiveDateTime, weekday: Weekday) -> NaiveDateTime {
1758 let current = value.weekday().num_days_from_monday() as i64;
1759 let target = weekday.num_days_from_monday() as i64;
1760 let delta = (target - current).rem_euclid(7);
1761 midnight(value.date() + Duration::days(delta))
1762}
1763
1764#[runmat_macros::runtime_builtin(
1765 name = "dateshift",
1766 descriptor(crate::builtins::datetime::DATESHIFT_DESCRIPTOR),
1767 builtin_path = "crate::builtins::datetime",
1768 category = "datetime",
1769 summary = "Shift datetime values to calendar or clock boundaries.",
1770 keywords = "dateshift,datetime,start,end,nearest,week,month,year",
1771 related = "datetime,year,month,day"
1772)]
1773async fn dateshift_builtin(
1774 value: Value,
1775 boundary: Value,
1776 unit: Value,
1777 rest: Vec<Value>,
1778) -> crate::BuiltinResult<Value> {
1779 let value = gather_if_needed_async(&value)
1780 .await
1781 .map_err(|err| datetime_error(format!("dateshift: {}", err.message())))?;
1782 let boundary = gather_if_needed_async(&boundary)
1783 .await
1784 .map_err(|err| datetime_error(format!("dateshift: {}", err.message())))?;
1785 let unit = gather_if_needed_async(&unit)
1786 .await
1787 .map_err(|err| datetime_error(format!("dateshift: {}", err.message())))?;
1788 let rest = gather_args(&rest).await?;
1789 let serials = serials_from_datetime_value(&value)?;
1790 let format = datetime_format_from_value(&value);
1791 let boundary = DateShiftBoundary::parse(&boundary)?;
1792
1793 let mut out = Vec::with_capacity(serials.data.len());
1794 if boundary == DateShiftBoundary::DayOfWeek {
1795 if !rest.is_empty() {
1796 return Err(datetime_error(
1797 "dateshift: dayofweek boundary does not accept extra arguments",
1798 ));
1799 }
1800 let weekday = parse_weekday(&unit)?;
1801 for serial in &serials.data {
1802 out.push(datenum_from_naive(shift_to_dayofweek(
1803 naive_from_datenum(*serial)?,
1804 weekday,
1805 )));
1806 }
1807 } else {
1808 let unit = DateShiftUnit::parse(&unit)?;
1809 let week_start = if matches!(unit, DateShiftUnit::Week) {
1810 if rest.len() > 1 {
1811 return Err(datetime_error(
1812 "dateshift: week unit accepts at most one weekday argument",
1813 ));
1814 }
1815 rest.first()
1816 .map(parse_weekday)
1817 .transpose()?
1818 .unwrap_or(Weekday::Mon)
1819 } else {
1820 if !rest.is_empty() {
1821 return Err(datetime_error(
1822 "dateshift: extra arguments are only supported for week units",
1823 ));
1824 }
1825 Weekday::Mon
1826 };
1827 for serial in &serials.data {
1828 out.push(datenum_from_naive(shift_naive_datetime(
1829 naive_from_datenum(*serial)?,
1830 boundary,
1831 unit,
1832 week_start,
1833 )));
1834 }
1835 }
1836
1837 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
1838 datetime_object_from_serials(out, shape, format)
1839}
1840
1841pub fn datetime_char_array(value: &Value) -> BuiltinResult<Option<CharArray>> {
1842 let Some(array) = datetime_string_array(value)? else {
1843 return Ok(None);
1844 };
1845 let width = array
1846 .data
1847 .iter()
1848 .map(|s| s.chars().count())
1849 .max()
1850 .unwrap_or(0);
1851 let rows = array.data.len();
1852 let mut data = vec![' '; rows * width];
1853 for (row, text) in array.data.iter().enumerate() {
1854 for (col, ch) in text.chars().enumerate() {
1855 data[row * width + col] = ch;
1856 }
1857 }
1858 let out = CharArray::new(data, rows, width)
1859 .map_err(|err| datetime_error(format!("datetime: {err}")))?;
1860 Ok(Some(out))
1861}
1862
1863#[cfg(test)]
1864mod tests {
1865 use super::*;
1866
1867 fn run_datetime(args: Vec<Value>) -> Value {
1868 futures::executor::block_on(datetime_builtin(args)).expect("datetime")
1869 }
1870
1871 fn as_datetime(value: Value) -> ObjectInstance {
1872 match value {
1873 Value::Object(object) => object,
1874 other => panic!("expected datetime object, got {other:?}"),
1875 }
1876 }
1877
1878 #[test]
1879 fn datetime_descriptor_signatures_cover_constructor_and_methods() {
1880 let labels: Vec<&str> = DATETIME_DESCRIPTOR
1881 .signatures
1882 .iter()
1883 .map(|sig| sig.label)
1884 .collect();
1885 assert!(labels.contains(&"t = datetime()"));
1886 assert!(labels.contains(&"t = datetime(year, month, day, hour, minute, second)"));
1887 assert!(labels.contains(&"t = datetime(serialDateNumbers, \"ConvertFrom\", \"datenum\")"));
1888
1889 assert_eq!(DATETIME_YEAR_DESCRIPTOR.signatures[0].label, "X = year(t)");
1890 assert_eq!(
1891 DATETIME_SUBSREF_DESCRIPTOR.signatures[0].label,
1892 "out = datetime.subsref(obj, kind, payload)"
1893 );
1894 assert_eq!(
1895 DATETIME_BINARY_DESCRIPTOR.signatures[0].label,
1896 "out = datetime.op(lhs, rhs)"
1897 );
1898 }
1899
1900 #[test]
1901 fn datetime_builds_from_components() {
1902 let value = run_datetime(vec![Value::Num(2024.0), Value::Num(3.0), Value::Num(14.0)]);
1903 let object = as_datetime(value);
1904 assert_eq!(object.class_name, DATETIME_CLASS);
1905 assert_eq!(format_for_object(&object), DEFAULT_DATE_FORMAT);
1906 let serials = serial_tensor_for_object(&object).expect("serials");
1907 assert_eq!(serials.data.len(), 1);
1908 let year =
1909 futures::executor::block_on(year_builtin(Value::Object(object.clone()))).expect("year");
1910 assert_eq!(year, Value::Num(2024.0));
1911 }
1912
1913 #[test]
1914 fn datetime_builds_arrays_from_component_vectors() {
1915 let years = Value::Tensor(Tensor::new(vec![2024.0, 2025.0], vec![1, 2]).unwrap());
1916 let months = Value::Tensor(Tensor::new(vec![1.0, 6.0], vec![1, 2]).unwrap());
1917 let days = Value::Tensor(Tensor::new(vec![15.0, 20.0], vec![1, 2]).unwrap());
1918 let value = run_datetime(vec![years, months, days]);
1919 let object = as_datetime(value.clone());
1920 let serials = serial_tensor_for_object(&object).expect("serials");
1921 assert_eq!(serials.shape, vec![1, 2]);
1922 let rendered = datetime_display_text(&value)
1923 .expect("display")
1924 .expect("datetime text");
1925 assert!(rendered.contains("15-Jan-2024"));
1926 assert!(rendered.contains("20-Jun-2025"));
1927 }
1928
1929 #[test]
1930 fn datetime_parses_text_and_converts_to_strings() {
1931 let value = run_datetime(vec![Value::String("2024-03-14 09:26:53".to_string())]);
1932 let rendered = datetime_string_array(&value)
1933 .expect("string array")
1934 .expect("datetime strings");
1935 assert_eq!(rendered.data, vec!["14-Mar-2024 09:26:53".to_string()]);
1936 }
1937
1938 #[test]
1939 fn datetime_accepts_existing_datetime_input() {
1940 let value = run_datetime(vec![Value::String("2024-03-14".to_string())]);
1941 let converted = run_datetime(vec![
1942 value.clone(),
1943 Value::from("InputFormat"),
1944 Value::from("yyyy-MM-dd"),
1945 ]);
1946 assert_eq!(
1947 serials_from_datetime_value(&converted).unwrap().data,
1948 serials_from_datetime_value(&value).unwrap().data
1949 );
1950 }
1951
1952 #[test]
1953 fn datetime_parses_text_with_input_format() {
1954 let input = Value::StringArray(
1955 StringArray::new(
1956 vec!["2024/03/14".to_string(), "2024/03/15".to_string()],
1957 vec![2, 1],
1958 )
1959 .unwrap(),
1960 );
1961 let value = run_datetime(vec![
1962 input,
1963 Value::from("InputFormat"),
1964 Value::from("yyyy/MM/dd"),
1965 Value::from("Format"),
1966 Value::from("yyyy-MM-dd"),
1967 ]);
1968 let rendered = datetime_string_array(&value)
1969 .expect("string array")
1970 .expect("datetime strings");
1971 assert_eq!(
1972 rendered.data,
1973 vec!["2024-03-14".to_string(), "2024-03-15".to_string()]
1974 );
1975 }
1976
1977 #[test]
1978 fn dateshift_supports_start_of_week_and_month_end() {
1979 let input = run_datetime(vec![
1980 Value::StringArray(
1981 StringArray::new(
1982 vec!["2024-03-14".to_string(), "2024-03-18".to_string()],
1983 vec![2, 1],
1984 )
1985 .unwrap(),
1986 ),
1987 Value::from("Format"),
1988 Value::from("yyyy-MM-dd"),
1989 ]);
1990 let shifted = futures::executor::block_on(dateshift_builtin(
1991 input,
1992 Value::from("start"),
1993 Value::from("week"),
1994 Vec::new(),
1995 ))
1996 .expect("dateshift start week");
1997 let rendered = datetime_string_array(&shifted)
1998 .expect("string array")
1999 .expect("datetime strings");
2000 assert_eq!(
2001 rendered.data,
2002 vec!["2024-03-11".to_string(), "2024-03-18".to_string()]
2003 );
2004
2005 let month_end = futures::executor::block_on(dateshift_builtin(
2006 run_datetime(vec![
2007 Value::from("2024-02-10"),
2008 Value::from("Format"),
2009 Value::from("yyyy-MM-dd HH:mm:ss"),
2010 ]),
2011 Value::from("end"),
2012 Value::from("month"),
2013 Vec::new(),
2014 ))
2015 .expect("dateshift end month");
2016 let rendered = datetime_string_array(&month_end)
2017 .expect("string array")
2018 .expect("datetime strings");
2019 assert_eq!(rendered.data, vec!["2024-02-29 23:59:59".to_string()]);
2020 }
2021
2022 #[test]
2023 fn dateshift_rejects_unsupported_extra_arguments() {
2024 let input = run_datetime(vec![Value::from("2024-03-14")]);
2025 let err = futures::executor::block_on(dateshift_builtin(
2026 input.clone(),
2027 Value::from("dayofweek"),
2028 Value::from("monday"),
2029 vec![Value::from("extra")],
2030 ))
2031 .expect_err("dayofweek extra argument should fail");
2032 assert!(err.message().contains("does not accept extra arguments"));
2033
2034 let err = futures::executor::block_on(dateshift_builtin(
2035 input.clone(),
2036 Value::from("start"),
2037 Value::from("week"),
2038 vec![Value::from("monday"), Value::from("extra")],
2039 ))
2040 .expect_err("week second extra argument should fail");
2041 assert!(err.message().contains("at most one weekday argument"));
2042
2043 let err = futures::executor::block_on(dateshift_builtin(
2044 input,
2045 Value::from("start"),
2046 Value::from("month"),
2047 vec![Value::from("monday")],
2048 ))
2049 .expect_err("non-week extra argument should fail");
2050 assert!(err
2051 .message()
2052 .contains("extra arguments are only supported for week units"));
2053 }
2054
2055 #[test]
2056 fn datetime_supports_format_assignment() {
2057 let value = run_datetime(vec![Value::Num(2024.0), Value::Num(3.0), Value::Num(14.0)]);
2058 let updated = futures::executor::block_on(datetime_subsasgn(
2059 value,
2060 ".".to_string(),
2061 Value::String(FORMAT_FIELD.to_string()),
2062 Value::String("yyyy-MM-dd".to_string()),
2063 ))
2064 .expect("subsasgn");
2065 let rendered = datetime_display_text(&updated)
2066 .expect("display")
2067 .expect("datetime text");
2068 assert_eq!(rendered, "2024-03-14");
2069 }
2070
2071 #[test]
2072 fn datetime_supports_indexing_and_comparison() {
2073 let years = Value::Tensor(Tensor::new(vec![2024.0, 2025.0], vec![1, 2]).unwrap());
2074 let months = Value::Tensor(Tensor::new(vec![1.0, 6.0], vec![1, 2]).unwrap());
2075 let days = Value::Tensor(Tensor::new(vec![15.0, 20.0], vec![1, 2]).unwrap());
2076 let value = run_datetime(vec![years, months, days]);
2077 let payload =
2078 Value::Cell(runmat_builtins::CellArray::new(vec![Value::Num(2.0)], 1, 1).unwrap());
2079 let indexed =
2080 futures::executor::block_on(datetime_subsref(value.clone(), "()".to_string(), payload))
2081 .expect("subsref");
2082 let year = futures::executor::block_on(year_builtin(indexed)).expect("year");
2083 assert_eq!(year, Value::Num(2025.0));
2084
2085 let lhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(1.0)]);
2086 let rhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(2.0)]);
2087 let cmp = futures::executor::block_on(datetime_lt(lhs, rhs)).expect("lt");
2088 assert_eq!(cmp, Value::Num(1.0));
2089 }
2090
2091 #[test]
2092 fn datetime_and_duration_interoperate() {
2093 let lhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(1.0)]);
2094 let rhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(2.0)]);
2095 let delta = futures::executor::block_on(datetime_minus(rhs.clone(), lhs.clone()))
2096 .expect("datetime minus datetime");
2097 assert_eq!(delta, Value::Num(1.0));
2098
2099 let duration = crate::builtins::duration::duration_object_from_days_tensor(
2100 Tensor::new(vec![1.0], vec![1, 1]).unwrap(),
2101 crate::builtins::duration::DEFAULT_DURATION_FORMAT,
2102 )
2103 .expect("duration");
2104
2105 let round_trip = futures::executor::block_on(datetime_plus(lhs.clone(), duration.clone()))
2106 .expect("plus");
2107 let round_trip_text = datetime_display_text(&round_trip)
2108 .expect("datetime display")
2109 .expect("datetime text");
2110 assert_eq!(round_trip_text, "02-Jan-2024");
2111
2112 let restored =
2113 futures::executor::block_on(datetime_minus(rhs, duration)).expect("minus duration");
2114 let restored_text = datetime_display_text(&restored)
2115 .expect("datetime display")
2116 .expect("datetime text");
2117 assert_eq!(restored_text, "01-Jan-2024");
2118 }
2119}