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