1use std::collections::HashMap;
2use std::sync::OnceLock;
3
4use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, Timelike};
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 DATETIME_SUBSREF_INPUTS: [BuiltinParamDescriptor; 3] = [
104 BuiltinParamDescriptor {
105 name: "obj",
106 ty: BuiltinParamType::Any,
107 arity: BuiltinParamArity::Required,
108 default: None,
109 description: "Datetime receiver object.",
110 },
111 BuiltinParamDescriptor {
112 name: "kind",
113 ty: BuiltinParamType::StringScalar,
114 arity: BuiltinParamArity::Required,
115 default: None,
116 description: "Indexing kind token.",
117 },
118 BuiltinParamDescriptor {
119 name: "payload",
120 ty: BuiltinParamType::Any,
121 arity: BuiltinParamArity::Required,
122 default: None,
123 description: "Index/member payload.",
124 },
125];
126const DATETIME_SUBSASGN_INPUTS: [BuiltinParamDescriptor; 4] = [
127 BuiltinParamDescriptor {
128 name: "obj",
129 ty: BuiltinParamType::Any,
130 arity: BuiltinParamArity::Required,
131 default: None,
132 description: "Datetime receiver object.",
133 },
134 BuiltinParamDescriptor {
135 name: "kind",
136 ty: BuiltinParamType::StringScalar,
137 arity: BuiltinParamArity::Required,
138 default: None,
139 description: "Indexing kind token.",
140 },
141 BuiltinParamDescriptor {
142 name: "payload",
143 ty: BuiltinParamType::Any,
144 arity: BuiltinParamArity::Required,
145 default: None,
146 description: "Index/member payload.",
147 },
148 BuiltinParamDescriptor {
149 name: "rhs",
150 ty: BuiltinParamType::Any,
151 arity: BuiltinParamArity::Required,
152 default: None,
153 description: "Assigned value.",
154 },
155];
156
157const DATETIME_SIGNATURES: [BuiltinSignatureDescriptor; 10] = [
158 BuiltinSignatureDescriptor {
159 label: "t = datetime()",
160 inputs: &[],
161 outputs: &OUT_DATETIME,
162 },
163 BuiltinSignatureDescriptor {
164 label: "t = datetime(textOrArray)",
165 inputs: &[BuiltinParamDescriptor {
166 name: "textOrArray",
167 ty: BuiltinParamType::Any,
168 arity: BuiltinParamArity::Required,
169 default: None,
170 description: "String/char/date text input.",
171 }],
172 outputs: &OUT_DATETIME,
173 },
174 BuiltinSignatureDescriptor {
175 label: "t = datetime(serialDateNumbers)",
176 inputs: &[BuiltinParamDescriptor {
177 name: "serialDateNumbers",
178 ty: BuiltinParamType::NumericArray,
179 arity: BuiltinParamArity::Required,
180 default: None,
181 description: "Numeric serial date input.",
182 }],
183 outputs: &OUT_DATETIME,
184 },
185 BuiltinSignatureDescriptor {
186 label: "t = datetime(year, month, day)",
187 inputs: &[
188 BuiltinParamDescriptor {
189 name: "year",
190 ty: BuiltinParamType::NumericArray,
191 arity: BuiltinParamArity::Required,
192 default: None,
193 description: "Year component.",
194 },
195 BuiltinParamDescriptor {
196 name: "month",
197 ty: BuiltinParamType::NumericArray,
198 arity: BuiltinParamArity::Required,
199 default: None,
200 description: "Month component.",
201 },
202 BuiltinParamDescriptor {
203 name: "day",
204 ty: BuiltinParamType::NumericArray,
205 arity: BuiltinParamArity::Required,
206 default: None,
207 description: "Day component.",
208 },
209 ],
210 outputs: &OUT_DATETIME,
211 },
212 BuiltinSignatureDescriptor {
213 label: "t = datetime(year, month, day, hour)",
214 inputs: &[
215 BuiltinParamDescriptor {
216 name: "year",
217 ty: BuiltinParamType::NumericArray,
218 arity: BuiltinParamArity::Required,
219 default: None,
220 description: "Year component.",
221 },
222 BuiltinParamDescriptor {
223 name: "month",
224 ty: BuiltinParamType::NumericArray,
225 arity: BuiltinParamArity::Required,
226 default: None,
227 description: "Month component.",
228 },
229 BuiltinParamDescriptor {
230 name: "day",
231 ty: BuiltinParamType::NumericArray,
232 arity: BuiltinParamArity::Required,
233 default: None,
234 description: "Day component.",
235 },
236 BuiltinParamDescriptor {
237 name: "hour",
238 ty: BuiltinParamType::NumericArray,
239 arity: BuiltinParamArity::Required,
240 default: None,
241 description: "Hour component.",
242 },
243 ],
244 outputs: &OUT_DATETIME,
245 },
246 BuiltinSignatureDescriptor {
247 label: "t = datetime(year, month, day, hour, minute)",
248 inputs: &[
249 BuiltinParamDescriptor {
250 name: "year",
251 ty: BuiltinParamType::NumericArray,
252 arity: BuiltinParamArity::Required,
253 default: None,
254 description: "Year component.",
255 },
256 BuiltinParamDescriptor {
257 name: "month",
258 ty: BuiltinParamType::NumericArray,
259 arity: BuiltinParamArity::Required,
260 default: None,
261 description: "Month component.",
262 },
263 BuiltinParamDescriptor {
264 name: "day",
265 ty: BuiltinParamType::NumericArray,
266 arity: BuiltinParamArity::Required,
267 default: None,
268 description: "Day component.",
269 },
270 BuiltinParamDescriptor {
271 name: "hour",
272 ty: BuiltinParamType::NumericArray,
273 arity: BuiltinParamArity::Required,
274 default: None,
275 description: "Hour component.",
276 },
277 BuiltinParamDescriptor {
278 name: "minute",
279 ty: BuiltinParamType::NumericArray,
280 arity: BuiltinParamArity::Required,
281 default: None,
282 description: "Minute component.",
283 },
284 ],
285 outputs: &OUT_DATETIME,
286 },
287 BuiltinSignatureDescriptor {
288 label: "t = datetime(year, month, day, hour, minute, second)",
289 inputs: &[
290 BuiltinParamDescriptor {
291 name: "year",
292 ty: BuiltinParamType::NumericArray,
293 arity: BuiltinParamArity::Required,
294 default: None,
295 description: "Year component.",
296 },
297 BuiltinParamDescriptor {
298 name: "month",
299 ty: BuiltinParamType::NumericArray,
300 arity: BuiltinParamArity::Required,
301 default: None,
302 description: "Month component.",
303 },
304 BuiltinParamDescriptor {
305 name: "day",
306 ty: BuiltinParamType::NumericArray,
307 arity: BuiltinParamArity::Required,
308 default: None,
309 description: "Day component.",
310 },
311 BuiltinParamDescriptor {
312 name: "hour",
313 ty: BuiltinParamType::NumericArray,
314 arity: BuiltinParamArity::Required,
315 default: None,
316 description: "Hour component.",
317 },
318 BuiltinParamDescriptor {
319 name: "minute",
320 ty: BuiltinParamType::NumericArray,
321 arity: BuiltinParamArity::Required,
322 default: None,
323 description: "Minute component.",
324 },
325 BuiltinParamDescriptor {
326 name: "second",
327 ty: BuiltinParamType::NumericArray,
328 arity: BuiltinParamArity::Required,
329 default: None,
330 description: "Second component.",
331 },
332 ],
333 outputs: &OUT_DATETIME,
334 },
335 BuiltinSignatureDescriptor {
336 label: "t = datetime(serialDateNumbers, \"ConvertFrom\", \"datenum\")",
337 inputs: &[BuiltinParamDescriptor {
338 name: "args",
339 ty: BuiltinParamType::Any,
340 arity: BuiltinParamArity::Variadic,
341 default: None,
342 description: "Numeric serial input with ConvertFrom option.",
343 }],
344 outputs: &OUT_DATETIME,
345 },
346 BuiltinSignatureDescriptor {
347 label: "t = datetime(___, \"Format\", format)",
348 inputs: &DATETIME_ARGS_ONLY,
349 outputs: &OUT_DATETIME,
350 },
351 BuiltinSignatureDescriptor {
352 label: "t = datetime(___, Name, Value, ...)",
353 inputs: &DATETIME_ARGS_ONLY,
354 outputs: &OUT_DATETIME,
355 },
356];
357
358const DATETIME_YEAR_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
359 label: "X = year(t)",
360 inputs: &DATETIME_SINGLE_INPUT,
361 outputs: &OUT_NUMERIC,
362}];
363const DATETIME_MONTH_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
364 label: "X = month(t)",
365 inputs: &DATETIME_SINGLE_INPUT,
366 outputs: &OUT_NUMERIC,
367}];
368const DATETIME_DAY_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
369 label: "X = day(t)",
370 inputs: &DATETIME_SINGLE_INPUT,
371 outputs: &OUT_NUMERIC,
372}];
373const DATETIME_HOUR_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
374 label: "X = hour(t)",
375 inputs: &DATETIME_SINGLE_INPUT,
376 outputs: &OUT_NUMERIC,
377}];
378const DATETIME_MINUTE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
379 label: "X = minute(t)",
380 inputs: &DATETIME_SINGLE_INPUT,
381 outputs: &OUT_NUMERIC,
382}];
383const DATETIME_SECOND_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
384 label: "X = second(t)",
385 inputs: &DATETIME_SINGLE_INPUT,
386 outputs: &OUT_NUMERIC,
387}];
388const DATETIME_SUBSREF_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
389 label: "out = datetime.subsref(obj, kind, payload)",
390 inputs: &DATETIME_SUBSREF_INPUTS,
391 outputs: &OUT_ANY,
392}];
393const DATETIME_SUBSASGN_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
394 [BuiltinSignatureDescriptor {
395 label: "out = datetime.subsasgn(obj, kind, payload, rhs)",
396 inputs: &DATETIME_SUBSASGN_INPUTS,
397 outputs: &OUT_ANY,
398 }];
399const DATETIME_BINARY_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
400 label: "out = datetime.op(lhs, rhs)",
401 inputs: &DATETIME_BINARY_INPUTS,
402 outputs: &OUT_ANY,
403}];
404
405pub const DATETIME_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
406 signatures: &DATETIME_SIGNATURES,
407 output_mode: BuiltinOutputMode::Fixed,
408 completion_policy: BuiltinCompletionPolicy::Public,
409 errors: &DATETIME_ERRORS,
410};
411pub const DATETIME_YEAR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
412 signatures: &DATETIME_YEAR_SIGNATURES,
413 output_mode: BuiltinOutputMode::Fixed,
414 completion_policy: BuiltinCompletionPolicy::Public,
415 errors: &DATETIME_ERRORS,
416};
417pub const DATETIME_MONTH_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
418 signatures: &DATETIME_MONTH_SIGNATURES,
419 output_mode: BuiltinOutputMode::Fixed,
420 completion_policy: BuiltinCompletionPolicy::Public,
421 errors: &DATETIME_ERRORS,
422};
423pub const DATETIME_DAY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
424 signatures: &DATETIME_DAY_SIGNATURES,
425 output_mode: BuiltinOutputMode::Fixed,
426 completion_policy: BuiltinCompletionPolicy::Public,
427 errors: &DATETIME_ERRORS,
428};
429pub const DATETIME_HOUR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
430 signatures: &DATETIME_HOUR_SIGNATURES,
431 output_mode: BuiltinOutputMode::Fixed,
432 completion_policy: BuiltinCompletionPolicy::Public,
433 errors: &DATETIME_ERRORS,
434};
435pub const DATETIME_MINUTE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
436 signatures: &DATETIME_MINUTE_SIGNATURES,
437 output_mode: BuiltinOutputMode::Fixed,
438 completion_policy: BuiltinCompletionPolicy::Public,
439 errors: &DATETIME_ERRORS,
440};
441pub const DATETIME_SECOND_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
442 signatures: &DATETIME_SECOND_SIGNATURES,
443 output_mode: BuiltinOutputMode::Fixed,
444 completion_policy: BuiltinCompletionPolicy::Public,
445 errors: &DATETIME_ERRORS,
446};
447pub const DATETIME_SUBSREF_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
448 signatures: &DATETIME_SUBSREF_SIGNATURES,
449 output_mode: BuiltinOutputMode::Fixed,
450 completion_policy: BuiltinCompletionPolicy::MethodOnly,
451 errors: &DATETIME_ERRORS,
452};
453pub const DATETIME_SUBSASGN_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
454 signatures: &DATETIME_SUBSASGN_SIGNATURES,
455 output_mode: BuiltinOutputMode::Fixed,
456 completion_policy: BuiltinCompletionPolicy::MethodOnly,
457 errors: &DATETIME_ERRORS,
458};
459pub const DATETIME_BINARY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
460 signatures: &DATETIME_BINARY_SIGNATURES,
461 output_mode: BuiltinOutputMode::Fixed,
462 completion_policy: BuiltinCompletionPolicy::MethodOnly,
463 errors: &DATETIME_ERRORS,
464};
465
466fn datetime_error(message: impl Into<String>) -> RuntimeError {
467 build_runtime_error(message)
468 .with_builtin(BUILTIN_NAME)
469 .build()
470}
471
472fn ensure_datetime_class_registered() {
473 DATETIME_CLASS_REGISTERED.get_or_init(|| {
474 let mut properties = HashMap::new();
475 properties.insert(
476 FORMAT_FIELD.to_string(),
477 PropertyDef {
478 name: FORMAT_FIELD.to_string(),
479 is_static: false,
480 is_constant: false,
481 is_dependent: false,
482 get_access: Access::Public,
483 set_access: Access::Public,
484 default_value: Some(Value::String(DEFAULT_DATETIME_FORMAT.to_string())),
485 },
486 );
487
488 let mut methods = HashMap::new();
489 for name in [
490 OBJECT_SUBSREF_METHOD,
491 OBJECT_SUBSASGN_METHOD,
492 "plus",
493 "minus",
494 "eq",
495 "ne",
496 "lt",
497 "le",
498 "gt",
499 "ge",
500 ] {
501 methods.insert(
502 name.to_string(),
503 MethodDef {
504 name: name.to_string(),
505 is_static: false,
506 is_abstract: false,
507 is_sealed: false,
508 access: Access::Public,
509 function_name: format!("{DATETIME_CLASS}.{name}"),
510 implicit_class_argument: None,
511 },
512 );
513 }
514
515 runmat_builtins::register_class(ClassDef {
516 name: DATETIME_CLASS.to_string(),
517 parent: None,
518 properties,
519 methods,
520 });
521 });
522}
523
524async fn gather_args(args: &[Value]) -> BuiltinResult<Vec<Value>> {
525 let mut out = Vec::with_capacity(args.len());
526 for arg in args {
527 out.push(
528 gather_if_needed_async(arg)
529 .await
530 .map_err(|err| datetime_error(format!("datetime: {}", err.message())))?,
531 );
532 }
533 Ok(out)
534}
535
536fn scalar_text(value: &Value, context: &str) -> BuiltinResult<String> {
537 match value {
538 Value::String(text) => Ok(text.clone()),
539 Value::StringArray(array) if array.data.len() == 1 => Ok(array.data[0].clone()),
540 Value::CharArray(array) if array.rows == 1 => Ok(array.data.iter().collect()),
541 _ => Err(datetime_error(format!(
542 "datetime: {context} must be a string scalar or character vector"
543 ))),
544 }
545}
546
547fn parse_trailing_options(
548 args: &[Value],
549) -> BuiltinResult<(usize, Option<String>, Option<String>)> {
550 let mut positional_end = args.len();
551 let mut format = None;
552 let mut convert_from = None;
553
554 while positional_end >= 2 {
555 let name = match scalar_text(&args[positional_end - 2], "option name") {
556 Ok(text) => text,
557 Err(_) => break,
558 };
559 let lowered = name.trim().to_ascii_lowercase();
560 let value = scalar_text(&args[positional_end - 1], &format!("{name} option"))?;
561 match lowered.as_str() {
562 "format" => format = Some(value),
563 "convertfrom" => convert_from = Some(value),
564 _ => break,
565 }
566 positional_end -= 2;
567 }
568
569 Ok((positional_end, format, convert_from))
570}
571
572fn tensor_from_numeric(value: Value, context: &str) -> BuiltinResult<Tensor> {
573 tensor::value_into_tensor_for(context, value)
574 .map_err(|message| datetime_error(format!("datetime: {message}")))
575}
576
577fn serial_tensor_from_value(value: Value, context: &str) -> BuiltinResult<Tensor> {
578 let tensor = tensor_from_numeric(value, context)?;
579 Tensor::new(
580 tensor.data.clone(),
581 tensor::default_shape_for(&tensor.shape, tensor.data.len()),
582 )
583 .map_err(|err| datetime_error(format!("datetime: {err}")))
584}
585
586fn format_for_object(obj: &ObjectInstance) -> String {
587 match obj.properties.get(FORMAT_FIELD) {
588 Some(Value::String(text)) => text.clone(),
589 Some(Value::StringArray(array)) if array.data.len() == 1 => array.data[0].clone(),
590 Some(Value::CharArray(array)) if array.rows == 1 => array.data.iter().collect(),
591 _ => DEFAULT_DATETIME_FORMAT.to_string(),
592 }
593}
594
595fn serial_tensor_for_object(obj: &ObjectInstance) -> BuiltinResult<Tensor> {
596 match obj.properties.get(SERIAL_FIELD) {
597 Some(Value::Tensor(tensor)) => Ok(tensor.clone()),
598 Some(Value::Num(value)) => Tensor::new(vec![*value], vec![1, 1])
599 .map_err(|err| datetime_error(format!("datetime: {err}"))),
600 Some(other) => Err(datetime_error(format!(
601 "datetime: invalid internal serial storage {other:?}"
602 ))),
603 None => Err(datetime_error("datetime: missing internal serial storage")),
604 }
605}
606
607pub(crate) fn datetime_object_from_serial_tensor(
608 serials: Tensor,
609 format: impl Into<String>,
610) -> BuiltinResult<Value> {
611 ensure_datetime_class_registered();
612 let mut object = ObjectInstance::new(DATETIME_CLASS.to_string());
613 object
614 .properties
615 .insert(SERIAL_FIELD.to_string(), Value::Tensor(serials));
616 object
617 .properties
618 .insert(FORMAT_FIELD.to_string(), Value::String(format.into()));
619 Ok(Value::Object(object))
620}
621
622fn datetime_object_from_serials(
623 serials: Vec<f64>,
624 shape: Vec<usize>,
625 format: impl Into<String>,
626) -> BuiltinResult<Value> {
627 let tensor =
628 Tensor::new(serials, shape).map_err(|err| datetime_error(format!("datetime: {err}")))?;
629 datetime_object_from_serial_tensor(tensor, format)
630}
631
632fn format_token_to_strftime(format: &str) -> String {
633 let mut out = format.to_string();
634 for (src, dst) in [
635 ("yyyy", "%Y"),
636 ("MMM", "%b"),
637 ("MM", "%m"),
638 ("dd", "%d"),
639 ("HH", "%H"),
640 ("mm", "%M"),
641 ("ss", "%S"),
642 ] {
643 out = out.replace(src, dst);
644 }
645 out
646}
647
648fn datenum_from_naive(datetime: NaiveDateTime) -> f64 {
649 let base = NaiveDate::from_ymd_opt(1970, 1, 1)
650 .unwrap()
651 .and_hms_opt(0, 0, 0)
652 .unwrap();
653 let duration = datetime - base;
654 let seconds = duration.num_seconds();
655 let nanos = (duration - Duration::seconds(seconds))
656 .num_nanoseconds()
657 .unwrap_or(0);
658 let total_seconds = seconds as f64 + nanos as f64 / 1_000_000_000.0;
659 total_seconds / SECONDS_PER_DAY + UNIX_DATENUM
660}
661
662fn naive_from_datenum(serial: f64) -> BuiltinResult<NaiveDateTime> {
663 if !serial.is_finite() {
664 return Err(datetime_error(
665 "datetime: serial date numbers must be finite",
666 ));
667 }
668 let total_seconds = (serial - UNIX_DATENUM) * SECONDS_PER_DAY;
669 let whole_seconds = total_seconds.floor();
670 let mut nanos = ((total_seconds - whole_seconds) * 1_000_000_000.0).round() as i64;
671 let mut seconds = whole_seconds as i64;
672 if nanos == 1_000_000_000 {
673 seconds += 1;
674 nanos = 0;
675 }
676 let base = NaiveDate::from_ymd_opt(1970, 1, 1)
677 .unwrap()
678 .and_hms_opt(0, 0, 0)
679 .unwrap();
680 Ok(base + Duration::seconds(seconds) + Duration::nanoseconds(nanos))
681}
682
683fn format_serial(serial: f64, format: &str) -> BuiltinResult<String> {
684 let naive = naive_from_datenum(serial)?;
685 let chrono_format = format_token_to_strftime(format);
686 Ok(naive.format(&chrono_format).to_string())
687}
688
689fn parse_datetime_text(text: &str) -> Option<(NaiveDateTime, bool)> {
690 let trimmed = text.trim();
691 if trimmed.is_empty() {
692 return None;
693 }
694
695 if let Ok(value) = DateTime::parse_from_rfc3339(trimmed) {
696 return Some((value.with_timezone(&Local).naive_local(), true));
697 }
698
699 for (pattern, has_time) in [
700 ("%Y-%m-%d %H:%M:%S", true),
701 ("%Y-%m-%d", false),
702 ("%d-%b-%Y %H:%M:%S", true),
703 ("%d-%b-%Y", false),
704 ("%m/%d/%Y %H:%M:%S", true),
705 ("%m/%d/%Y", false),
706 ] {
707 if has_time {
708 if let Ok(value) = NaiveDateTime::parse_from_str(trimmed, pattern) {
709 return Some((value, true));
710 }
711 } else if let Ok(value) = NaiveDate::parse_from_str(trimmed, pattern) {
712 return Some((value.and_hms_opt(0, 0, 0).unwrap(), false));
713 }
714 }
715
716 None
717}
718
719fn parse_text_input(value: Value) -> BuiltinResult<(Vec<f64>, Vec<usize>, String)> {
720 match value {
721 Value::String(text) => {
722 if text.trim().eq_ignore_ascii_case("now") {
723 let now = Local::now().naive_local();
724 return Ok((
725 vec![datenum_from_naive(now)],
726 vec![1, 1],
727 DEFAULT_DATETIME_FORMAT.to_string(),
728 ));
729 }
730 let (naive, has_time) = parse_datetime_text(&text).ok_or_else(|| {
731 datetime_error(format!("datetime: unable to parse date/time text '{text}'"))
732 })?;
733 Ok((
734 vec![datenum_from_naive(naive)],
735 vec![1, 1],
736 if has_time {
737 DEFAULT_DATETIME_FORMAT.to_string()
738 } else {
739 DEFAULT_DATE_FORMAT.to_string()
740 },
741 ))
742 }
743 Value::StringArray(array) => {
744 let mut serials = Vec::with_capacity(array.data.len());
745 let mut has_time = false;
746 for text in &array.data {
747 let (naive, parsed_has_time) = parse_datetime_text(text).ok_or_else(|| {
748 datetime_error(format!("datetime: unable to parse date/time text '{text}'"))
749 })?;
750 serials.push(datenum_from_naive(naive));
751 has_time |= parsed_has_time;
752 }
753 Ok((
754 serials,
755 tensor::default_shape_for(&array.shape, array.data.len()),
756 if has_time {
757 DEFAULT_DATETIME_FORMAT.to_string()
758 } else {
759 DEFAULT_DATE_FORMAT.to_string()
760 },
761 ))
762 }
763 Value::CharArray(array) => {
764 let mut texts = Vec::with_capacity(array.rows);
765 for row in 0..array.rows {
766 let start = row * array.cols;
767 let end = start + array.cols;
768 texts.push(
769 array.data[start..end]
770 .iter()
771 .collect::<String>()
772 .trim_end()
773 .to_string(),
774 );
775 }
776 parse_text_input(Value::StringArray(
777 StringArray::new(texts, vec![array.rows, 1])
778 .map_err(|err| datetime_error(format!("datetime: {err}")))?,
779 ))
780 }
781 _ => Err(datetime_error(
782 "datetime: text input must be a string scalar, string array, or character array",
783 )),
784 }
785}
786
787fn round_component(value: f64, label: &str, min: i64, max: i64) -> BuiltinResult<i64> {
788 if !value.is_finite() {
789 return Err(datetime_error(format!(
790 "datetime: {label} values must be finite"
791 )));
792 }
793 let rounded = value.round();
794 if (rounded - value).abs() > 1e-9 {
795 return Err(datetime_error(format!(
796 "datetime: {label} values must be integers"
797 )));
798 }
799 let integer = rounded as i64;
800 if integer < min || integer > max {
801 return Err(datetime_error(format!(
802 "datetime: {label} values must be in the range [{min}, {max}]"
803 )));
804 }
805 Ok(integer)
806}
807
808fn naive_from_components(
809 year: f64,
810 month: f64,
811 day: f64,
812 hour: f64,
813 minute: f64,
814 second: f64,
815) -> BuiltinResult<NaiveDateTime> {
816 let year = round_component(year, "year", -262_000, 262_000)? as i32;
817 let month = round_component(month, "month", 1, 12)? as u32;
818 let day = round_component(day, "day", 1, 31)? as u32;
819 let hour = round_component(hour, "hour", 0, 23)? as u32;
820 let minute = round_component(minute, "minute", 0, 59)? as u32;
821 if !second.is_finite() {
822 return Err(datetime_error("datetime: second values must be finite"));
823 }
824 if !(0.0..60.0).contains(&second) {
825 return Err(datetime_error(
826 "datetime: second values must be in the range [0, 60)",
827 ));
828 }
829
830 let base_date = NaiveDate::from_ymd_opt(year, month, day)
831 .ok_or_else(|| datetime_error("datetime: invalid calendar date"))?;
832 let whole_second = second.floor();
833 let mut nanos = ((second - whole_second) * 1_000_000_000.0).round() as u32;
834 let mut secs = whole_second as u32;
835 if nanos == 1_000_000_000 {
836 secs += 1;
837 nanos = 0;
838 }
839 let time = base_date
840 .and_hms_nano_opt(hour, minute, secs, nanos)
841 .ok_or_else(|| datetime_error("datetime: invalid time components"))?;
842 Ok(time)
843}
844
845fn broadcast_component_data(
846 arrays: &[Tensor],
847 labels: &[&str],
848) -> BuiltinResult<(Vec<Vec<f64>>, Vec<usize>)> {
849 let mut target_shape = vec![1, 1];
850 let mut target_len = 1usize;
851
852 for array in arrays {
853 let len = array.data.len();
854 if len > 1 {
855 let shape = tensor::default_shape_for(&array.shape, len);
856 if target_len == 1 {
857 target_len = len;
858 target_shape = shape;
859 } else if len != target_len || shape != target_shape {
860 return Err(datetime_error(
861 "datetime: non-scalar component inputs must have matching sizes",
862 ));
863 }
864 }
865 }
866
867 let mut broadcasted = Vec::with_capacity(arrays.len());
868 for (idx, array) in arrays.iter().enumerate() {
869 if array.data.len() == 1 {
870 broadcasted.push(vec![array.data[0]; target_len]);
871 } else if array.data.len() == target_len {
872 broadcasted.push(array.data.clone());
873 } else {
874 return Err(datetime_error(format!(
875 "datetime: {} input size does not match the other components",
876 labels[idx]
877 )));
878 }
879 }
880
881 Ok((broadcasted, target_shape))
882}
883
884fn component_tensor(value: Value, context: &str) -> BuiltinResult<Tensor> {
885 let tensor = tensor_from_numeric(value, context)?;
886 Tensor::new(
887 tensor.data.clone(),
888 tensor::default_shape_for(&tensor.shape, tensor.data.len()),
889 )
890 .map_err(|err| datetime_error(format!("datetime: {err}")))
891}
892
893fn build_from_components(args: Vec<Value>, format: Option<String>) -> BuiltinResult<Value> {
894 let labels = ["year", "month", "day", "hour", "minute", "second"];
895 let input_count = args.len();
896 let mut arrays = Vec::with_capacity(args.len());
897 for (idx, arg) in args.into_iter().enumerate() {
898 arrays.push(component_tensor(arg, labels[idx])?);
899 }
900 while arrays.len() < 6 {
901 arrays.push(Tensor::new(vec![0.0], vec![1, 1]).unwrap());
902 }
903
904 let (broadcasted, shape) = broadcast_component_data(&arrays, &labels)?;
905 let len = broadcasted[0].len();
906 let mut serials = Vec::with_capacity(len);
907 for idx in 0..len {
908 let naive = naive_from_components(
909 broadcasted[0][idx],
910 broadcasted[1][idx],
911 broadcasted[2][idx],
912 broadcasted[3][idx],
913 broadcasted[4][idx],
914 broadcasted[5][idx],
915 )?;
916 serials.push(datenum_from_naive(naive));
917 }
918
919 let default_format = if let Some(format) = format {
920 format
921 } else if input_count > 3 {
922 DEFAULT_DATETIME_FORMAT.to_string()
923 } else {
924 DEFAULT_DATE_FORMAT.to_string()
925 };
926 datetime_object_from_serials(serials, shape, default_format)
927}
928
929fn numeric_value_to_datetime(value: Value, format: Option<String>) -> BuiltinResult<Value> {
930 let serials = serial_tensor_from_value(value, "datetime")?;
931 datetime_object_from_serial_tensor(
932 serials,
933 format.unwrap_or_else(|| DEFAULT_DATETIME_FORMAT.to_string()),
934 )
935}
936
937pub fn is_datetime_object(value: &Value) -> bool {
938 matches!(value, Value::Object(obj) if obj.is_class(DATETIME_CLASS))
939}
940
941pub(crate) fn serials_from_datetime_value(value: &Value) -> BuiltinResult<Tensor> {
942 match value {
943 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => serial_tensor_for_object(obj),
944 _ => Err(datetime_error("datetime: expected a datetime value")),
945 }
946}
947
948pub(crate) fn datetime_format_from_value(value: &Value) -> String {
949 match value {
950 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => format_for_object(obj),
951 _ => DEFAULT_DATETIME_FORMAT.to_string(),
952 }
953}
954
955pub fn datetime_string_array(value: &Value) -> BuiltinResult<Option<StringArray>> {
956 let Value::Object(obj) = value else {
957 return Ok(None);
958 };
959 if !obj.is_class(DATETIME_CLASS) {
960 return Ok(None);
961 }
962 let serials = serial_tensor_for_object(obj)?;
963 let format = format_for_object(obj);
964 let mut strings = Vec::with_capacity(serials.data.len());
965 for serial in &serials.data {
966 strings.push(format_serial(*serial, &format)?);
967 }
968 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
969 let array = StringArray::new(strings, shape)
970 .map_err(|err| datetime_error(format!("datetime: {err}")))?;
971 Ok(Some(array))
972}
973
974pub fn datetime_display_text(value: &Value) -> BuiltinResult<Option<String>> {
975 let Some(array) = datetime_string_array(value)? else {
976 return Ok(None);
977 };
978 if array.data.len() == 1 {
979 return Ok(Some(array.data[0].clone()));
980 }
981
982 let rows = array.rows;
983 let cols = array.cols;
984 let mut widths = vec![0usize; cols];
985 for col in 0..cols {
986 for row in 0..rows {
987 let idx = row + col * rows;
988 widths[col] = widths[col].max(array.data[idx].chars().count());
989 }
990 }
991
992 let mut lines = Vec::with_capacity(rows);
993 for row in 0..rows {
994 let mut line = String::new();
995 for col in 0..cols {
996 if col > 0 {
997 line.push_str(" ");
998 }
999 let idx = row + col * rows;
1000 let text = &array.data[idx];
1001 line.push_str(text);
1002 let padding = widths[col].saturating_sub(text.chars().count());
1003 if padding > 0 {
1004 line.push_str(&" ".repeat(padding));
1005 }
1006 }
1007 lines.push(line);
1008 }
1009 Ok(Some(lines.join("\n")))
1010}
1011
1012pub fn datetime_summary(value: &Value) -> BuiltinResult<Option<String>> {
1013 let Value::Object(obj) = value else {
1014 return Ok(None);
1015 };
1016 if !obj.is_class(DATETIME_CLASS) {
1017 return Ok(None);
1018 }
1019 let serials = serial_tensor_for_object(obj)?;
1020 if serials.data.len() == 1 {
1021 return datetime_display_text(value);
1022 }
1023 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
1024 Ok(Some(format!(
1025 "[{} datetime]",
1026 shape
1027 .iter()
1028 .map(|dim| dim.to_string())
1029 .collect::<Vec<_>>()
1030 .join("x")
1031 )))
1032}
1033
1034fn component_tensor_from_datetime(
1035 value: &Value,
1036 label: &str,
1037 extractor: impl Fn(&NaiveDateTime) -> f64,
1038) -> BuiltinResult<Value> {
1039 let serials = serials_from_datetime_value(value)?;
1040 let mut out = Vec::with_capacity(serials.data.len());
1041 for serial in &serials.data {
1042 let naive = naive_from_datenum(*serial)?;
1043 out.push(extractor(&naive));
1044 }
1045 if out.len() == 1 {
1046 Ok(Value::Num(out[0]))
1047 } else {
1048 let shape = tensor::default_shape_for(&serials.shape, serials.data.len());
1049 let tensor =
1050 Tensor::new(out, shape).map_err(|err| datetime_error(format!("{label}: {err}")))?;
1051 Ok(Value::Tensor(tensor))
1052 }
1053}
1054
1055fn tensor_or_scalar(data: Vec<f64>, shape: Vec<usize>) -> BuiltinResult<Value> {
1056 if data.len() == 1 {
1057 Ok(Value::Num(data[0]))
1058 } else {
1059 Ok(Value::Tensor(Tensor::new(data, shape).map_err(|err| {
1060 datetime_error(format!("datetime: {err}"))
1061 })?))
1062 }
1063}
1064
1065async fn datetime_indexing(obj: Value, payload: Value) -> BuiltinResult<Value> {
1066 let Value::Object(object) = obj else {
1067 return Err(datetime_error(
1068 "datetime.subsref: receiver must be a datetime object",
1069 ));
1070 };
1071 let format = format_for_object(&object);
1072 let serials = serial_tensor_for_object(&object)?;
1073
1074 let Value::Cell(cell) = payload else {
1075 return Err(datetime_error(
1076 "datetime.subsref: indexing payload must be a cell array",
1077 ));
1078 };
1079 if cell.data.is_empty() {
1080 return datetime_object_from_serial_tensor(serials, format);
1081 }
1082 if cell.data.len() != 1 {
1083 return Err(datetime_error(
1084 "datetime.subsref: only linear datetime indexing is currently supported",
1085 ));
1086 }
1087 let selector = (*cell.data[0]).clone();
1088 let selector = match selector {
1089 Value::Tensor(tensor) => tensor,
1090 Value::Num(value) => Tensor::new(vec![value], vec![1, 1])
1091 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1092 Value::Int(value) => Tensor::new(vec![value.to_f64()], vec![1, 1])
1093 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1094 Value::LogicalArray(logical) => tensor::logical_to_tensor(&logical)
1095 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1096 other => {
1097 return Err(datetime_error(format!(
1098 "datetime.subsref: unsupported index value {other:?}"
1099 )))
1100 }
1101 };
1102 let indexed = crate::perform_indexing(&Value::Tensor(serials), &selector.data)
1103 .await
1104 .map_err(|err| datetime_error(format!("datetime.subsref: {}", err.message())))?;
1105 let indexed_serials = match indexed {
1106 Value::Num(value) => Tensor::new(vec![value], vec![1, 1])
1107 .map_err(|err| datetime_error(format!("datetime.subsref: {err}")))?,
1108 Value::Tensor(tensor) => tensor,
1109 other => {
1110 return Err(datetime_error(format!(
1111 "datetime.subsref: unexpected indexing result {other:?}"
1112 )))
1113 }
1114 };
1115 datetime_object_from_serial_tensor(indexed_serials, format)
1116}
1117
1118#[runmat_macros::runtime_builtin(
1119 name = "datetime",
1120 descriptor(crate::builtins::datetime::DATETIME_DESCRIPTOR),
1121 builtin_path = "crate::builtins::datetime",
1122 category = "datetime",
1123 summary = "Create datetime arrays from text, components, or serial date numbers.",
1124 keywords = "datetime,date,time,datenum,Format",
1125 related = "year,month,day,hour,minute,second,string,char,disp",
1126 examples = "t = datetime(2024, 4, 9, 13, 30, 0);"
1127)]
1128async fn datetime_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
1129 ensure_datetime_class_registered();
1130 let args = gather_args(&args).await?;
1131 let (positional_end, format, convert_from) = parse_trailing_options(&args)?;
1132 let positional = args[..positional_end].to_vec();
1133
1134 if let Some(convert_from) = convert_from {
1135 if !convert_from.eq_ignore_ascii_case("datenum") {
1136 return Err(datetime_error(format!(
1137 "datetime: unsupported ConvertFrom value '{convert_from}'"
1138 )));
1139 }
1140 if positional.len() != 1 {
1141 return Err(datetime_error(
1142 "datetime: ConvertFrom='datenum' expects exactly one numeric input",
1143 ));
1144 }
1145 return numeric_value_to_datetime(positional[0].clone(), format);
1146 }
1147
1148 match positional.len() {
1149 0 => {
1150 let now = Local::now().naive_local();
1151 datetime_object_from_serials(
1152 vec![datenum_from_naive(now)],
1153 vec![1, 1],
1154 format.unwrap_or_else(|| DEFAULT_DATETIME_FORMAT.to_string()),
1155 )
1156 }
1157 1 => match &positional[0] {
1158 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => {
1159 let (serials, shape, inferred_format) = parse_text_input(positional[0].clone())?;
1160 datetime_object_from_serials(serials, shape, format.unwrap_or(inferred_format))
1161 }
1162 _ => numeric_value_to_datetime(positional[0].clone(), format),
1163 },
1164 3..=6 => build_from_components(positional, format),
1165 _ => Err(datetime_error(
1166 "datetime: unsupported argument pattern; use text, serial dates, or Y/M/D component inputs",
1167 )),
1168 }
1169}
1170
1171#[runmat_macros::runtime_builtin(
1172 name = "year",
1173 descriptor(crate::builtins::datetime::DATETIME_YEAR_DESCRIPTOR),
1174 builtin_path = "crate::builtins::datetime",
1175 category = "datetime",
1176 summary = "Extract calendar year components from datetime values.",
1177 keywords = "year,datetime,date component"
1178)]
1179async fn year_builtin(value: Value) -> crate::BuiltinResult<Value> {
1180 component_tensor_from_datetime(&value, "year", |naive| naive.year() as f64)
1181}
1182
1183#[runmat_macros::runtime_builtin(
1184 name = "month",
1185 descriptor(crate::builtins::datetime::DATETIME_MONTH_DESCRIPTOR),
1186 builtin_path = "crate::builtins::datetime",
1187 category = "datetime",
1188 summary = "Extract month numbers from datetime arrays.",
1189 keywords = "month,datetime,date component"
1190)]
1191async fn month_builtin(value: Value) -> crate::BuiltinResult<Value> {
1192 component_tensor_from_datetime(&value, "month", |naive| naive.month() as f64)
1193}
1194
1195#[runmat_macros::runtime_builtin(
1196 name = "day",
1197 descriptor(crate::builtins::datetime::DATETIME_DAY_DESCRIPTOR),
1198 builtin_path = "crate::builtins::datetime",
1199 category = "datetime",
1200 summary = "Extract day-of-month numbers from datetime values.",
1201 keywords = "day,datetime,date component"
1202)]
1203async fn day_builtin(value: Value) -> crate::BuiltinResult<Value> {
1204 component_tensor_from_datetime(&value, "day", |naive| naive.day() as f64)
1205}
1206
1207#[runmat_macros::runtime_builtin(
1208 name = "hour",
1209 descriptor(crate::builtins::datetime::DATETIME_HOUR_DESCRIPTOR),
1210 builtin_path = "crate::builtins::datetime",
1211 category = "datetime",
1212 summary = "Extract hour components from datetime values.",
1213 keywords = "hour,datetime,time component"
1214)]
1215async fn hour_builtin(value: Value) -> crate::BuiltinResult<Value> {
1216 component_tensor_from_datetime(&value, "hour", |naive| naive.hour() as f64)
1217}
1218
1219#[runmat_macros::runtime_builtin(
1220 name = "minute",
1221 descriptor(crate::builtins::datetime::DATETIME_MINUTE_DESCRIPTOR),
1222 builtin_path = "crate::builtins::datetime",
1223 category = "datetime",
1224 summary = "Extract minute numbers from datetime arrays.",
1225 keywords = "minute,datetime,time component"
1226)]
1227async fn minute_builtin(value: Value) -> crate::BuiltinResult<Value> {
1228 component_tensor_from_datetime(&value, "minute", |naive| naive.minute() as f64)
1229}
1230
1231#[runmat_macros::runtime_builtin(
1232 name = "second",
1233 descriptor(crate::builtins::datetime::DATETIME_SECOND_DESCRIPTOR),
1234 builtin_path = "crate::builtins::datetime",
1235 category = "datetime",
1236 summary = "Extract second components from datetime values.",
1237 keywords = "second,datetime,time component"
1238)]
1239async fn second_builtin(value: Value) -> crate::BuiltinResult<Value> {
1240 component_tensor_from_datetime(&value, "second", |naive| {
1241 naive.second() as f64 + f64::from(naive.nanosecond()) / 1_000_000_000.0
1242 })
1243}
1244
1245#[runmat_macros::runtime_builtin(
1246 name = "datetime.subsref",
1247 descriptor(crate::builtins::datetime::DATETIME_SUBSREF_DESCRIPTOR),
1248 builtin_path = "crate::builtins::datetime"
1249)]
1250async fn datetime_subsref(obj: Value, kind: String, payload: Value) -> crate::BuiltinResult<Value> {
1251 match kind.as_str() {
1252 OBJECT_INDEX_PAREN => datetime_indexing(obj, payload).await,
1253 OBJECT_INDEX_MEMBER => {
1254 let Value::Object(object) = obj else {
1255 return Err(datetime_error(
1256 "datetime.subsref: receiver must be a datetime object",
1257 ));
1258 };
1259 let field = scalar_text(&payload, "field selector")?;
1260 match field.as_str() {
1261 FORMAT_FIELD => Ok(Value::String(format_for_object(&object))),
1262 _ => Err(datetime_error(format!(
1263 "datetime.subsref: unsupported datetime property '{field}'"
1264 ))),
1265 }
1266 }
1267 other => Err(datetime_error(format!(
1268 "datetime.subsref: unsupported indexing kind '{other}'"
1269 ))),
1270 }
1271}
1272
1273#[runmat_macros::runtime_builtin(
1274 name = "datetime.subsasgn",
1275 descriptor(crate::builtins::datetime::DATETIME_SUBSASGN_DESCRIPTOR),
1276 builtin_path = "crate::builtins::datetime"
1277)]
1278async fn datetime_subsasgn(
1279 obj: Value,
1280 kind: String,
1281 payload: Value,
1282 rhs: Value,
1283) -> crate::BuiltinResult<Value> {
1284 let Value::Object(mut object) = obj else {
1285 return Err(datetime_error(
1286 "datetime.subsasgn: receiver must be a datetime object",
1287 ));
1288 };
1289 match kind.as_str() {
1290 OBJECT_INDEX_MEMBER => {
1291 let field = scalar_text(&payload, "field selector")?;
1292 match field.as_str() {
1293 FORMAT_FIELD => {
1294 let text = scalar_text(&rhs, "Format value")?;
1295 object
1296 .properties
1297 .insert(FORMAT_FIELD.to_string(), Value::String(text));
1298 Ok(Value::Object(object))
1299 }
1300 _ => Err(datetime_error(format!(
1301 "datetime.subsasgn: unsupported datetime property '{field}'"
1302 ))),
1303 }
1304 }
1305 _ => Err(datetime_error(format!(
1306 "datetime.subsasgn: unsupported indexing kind '{kind}'"
1307 ))),
1308 }
1309}
1310
1311fn datetime_binary_serials(
1312 lhs: Value,
1313 rhs: Value,
1314 context: &str,
1315) -> BuiltinResult<(Tensor, Tensor, Vec<usize>, String)> {
1316 let lhs_serials = serials_from_datetime_value(&lhs)?;
1317 let rhs_serials = match &rhs {
1318 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => serial_tensor_for_object(obj)?,
1319 _ => serial_tensor_from_value(rhs, context)?,
1320 };
1321 let (left, right, shape) =
1322 tensor::binary_numeric_tensors(&lhs_serials, &rhs_serials, context, BUILTIN_NAME)?;
1323 let left_tensor = Tensor::new(left, shape.clone())
1324 .map_err(|err| datetime_error(format!("{context}: {err}")))?;
1325 let right_tensor = Tensor::new(right, shape.clone())
1326 .map_err(|err| datetime_error(format!("{context}: {err}")))?;
1327 Ok((
1328 left_tensor,
1329 right_tensor,
1330 shape,
1331 datetime_format_from_value(&lhs),
1332 ))
1333}
1334
1335fn compare_datetime(
1336 lhs: Value,
1337 rhs: Value,
1338 op: &str,
1339 cmp: impl Fn(f64, f64) -> bool,
1340) -> BuiltinResult<Value> {
1341 let (left, right, shape, _) = datetime_binary_serials(lhs, rhs, op)?;
1342 let out = left
1343 .data
1344 .iter()
1345 .zip(right.data.iter())
1346 .map(|(a, b)| if cmp(*a, *b) { 1.0 } else { 0.0 })
1347 .collect::<Vec<_>>();
1348 tensor_or_scalar(out, shape)
1349}
1350
1351#[runmat_macros::runtime_builtin(
1352 name = "datetime.eq",
1353 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1354 builtin_path = "crate::builtins::datetime"
1355)]
1356async fn datetime_eq(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1357 compare_datetime(lhs, rhs, "eq", |a, b| (a - b).abs() <= 1e-12)
1358}
1359
1360#[runmat_macros::runtime_builtin(
1361 name = "datetime.ne",
1362 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1363 builtin_path = "crate::builtins::datetime"
1364)]
1365async fn datetime_ne(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1366 compare_datetime(lhs, rhs, "ne", |a, b| (a - b).abs() > 1e-12)
1367}
1368
1369#[runmat_macros::runtime_builtin(
1370 name = "datetime.lt",
1371 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1372 builtin_path = "crate::builtins::datetime"
1373)]
1374async fn datetime_lt(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1375 compare_datetime(lhs, rhs, "lt", |a, b| a < b)
1376}
1377
1378#[runmat_macros::runtime_builtin(
1379 name = "datetime.le",
1380 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1381 builtin_path = "crate::builtins::datetime"
1382)]
1383async fn datetime_le(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1384 compare_datetime(lhs, rhs, "le", |a, b| a <= b)
1385}
1386
1387#[runmat_macros::runtime_builtin(
1388 name = "datetime.gt",
1389 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1390 builtin_path = "crate::builtins::datetime"
1391)]
1392async fn datetime_gt(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1393 compare_datetime(lhs, rhs, "gt", |a, b| a > b)
1394}
1395
1396#[runmat_macros::runtime_builtin(
1397 name = "datetime.ge",
1398 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1399 builtin_path = "crate::builtins::datetime"
1400)]
1401async fn datetime_ge(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1402 compare_datetime(lhs, rhs, "ge", |a, b| a >= b)
1403}
1404
1405#[runmat_macros::runtime_builtin(
1406 name = "datetime.plus",
1407 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1408 builtin_path = "crate::builtins::datetime"
1409)]
1410async fn datetime_plus(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1411 let lhs_serials = serials_from_datetime_value(&lhs)?;
1412 let rhs_numeric = if crate::builtins::duration::is_duration_object(&rhs) {
1413 crate::builtins::duration::duration_tensor_from_duration_value(&rhs)?
1414 } else {
1415 serial_tensor_from_value(rhs, "plus")?
1416 };
1417 let (left, right, shape) =
1418 tensor::binary_numeric_tensors(&lhs_serials, &rhs_numeric, "plus", BUILTIN_NAME)?;
1419 let serials = left
1420 .iter()
1421 .zip(right.iter())
1422 .map(|(a, b)| a + b)
1423 .collect::<Vec<_>>();
1424 datetime_object_from_serials(serials, shape, datetime_format_from_value(&lhs))
1425}
1426
1427#[runmat_macros::runtime_builtin(
1428 name = "datetime.minus",
1429 descriptor(crate::builtins::datetime::DATETIME_BINARY_DESCRIPTOR),
1430 builtin_path = "crate::builtins::datetime"
1431)]
1432async fn datetime_minus(lhs: Value, rhs: Value) -> crate::BuiltinResult<Value> {
1433 let lhs_serials = serials_from_datetime_value(&lhs)?;
1434 match &rhs {
1435 _ if crate::builtins::duration::is_duration_object(&rhs) => {
1436 let rhs_days = crate::builtins::duration::duration_tensor_from_duration_value(&rhs)?;
1437 let (left, right, shape) =
1438 tensor::binary_numeric_tensors(&lhs_serials, &rhs_days, "minus", BUILTIN_NAME)?;
1439 let serials = left
1440 .iter()
1441 .zip(right.iter())
1442 .map(|(a, b)| a - b)
1443 .collect::<Vec<_>>();
1444 datetime_object_from_serials(serials, shape, datetime_format_from_value(&lhs))
1445 }
1446 Value::Object(obj) if obj.is_class(DATETIME_CLASS) => {
1447 let rhs_serials = serial_tensor_for_object(obj)?;
1448 let (left, right, shape) =
1449 tensor::binary_numeric_tensors(&lhs_serials, &rhs_serials, "minus", BUILTIN_NAME)?;
1450 let deltas = left
1451 .iter()
1452 .zip(right.iter())
1453 .map(|(a, b)| a - b)
1454 .collect::<Vec<_>>();
1455 tensor_or_scalar(deltas, shape)
1456 }
1457 _ => {
1458 let rhs_numeric = serial_tensor_from_value(rhs, "minus")?;
1459 let (left, right, shape) =
1460 tensor::binary_numeric_tensors(&lhs_serials, &rhs_numeric, "minus", BUILTIN_NAME)?;
1461 let serials = left
1462 .iter()
1463 .zip(right.iter())
1464 .map(|(a, b)| a - b)
1465 .collect::<Vec<_>>();
1466 datetime_object_from_serials(serials, shape, datetime_format_from_value(&lhs))
1467 }
1468 }
1469}
1470
1471pub fn datetime_char_array(value: &Value) -> BuiltinResult<Option<CharArray>> {
1472 let Some(array) = datetime_string_array(value)? else {
1473 return Ok(None);
1474 };
1475 let width = array
1476 .data
1477 .iter()
1478 .map(|s| s.chars().count())
1479 .max()
1480 .unwrap_or(0);
1481 let rows = array.data.len();
1482 let mut data = vec![' '; rows * width];
1483 for (row, text) in array.data.iter().enumerate() {
1484 for (col, ch) in text.chars().enumerate() {
1485 data[row * width + col] = ch;
1486 }
1487 }
1488 let out = CharArray::new(data, rows, width)
1489 .map_err(|err| datetime_error(format!("datetime: {err}")))?;
1490 Ok(Some(out))
1491}
1492
1493#[cfg(test)]
1494mod tests {
1495 use super::*;
1496
1497 fn run_datetime(args: Vec<Value>) -> Value {
1498 futures::executor::block_on(datetime_builtin(args)).expect("datetime")
1499 }
1500
1501 fn as_datetime(value: Value) -> ObjectInstance {
1502 match value {
1503 Value::Object(object) => object,
1504 other => panic!("expected datetime object, got {other:?}"),
1505 }
1506 }
1507
1508 #[test]
1509 fn datetime_descriptor_signatures_cover_constructor_and_methods() {
1510 let labels: Vec<&str> = DATETIME_DESCRIPTOR
1511 .signatures
1512 .iter()
1513 .map(|sig| sig.label)
1514 .collect();
1515 assert!(labels.contains(&"t = datetime()"));
1516 assert!(labels.contains(&"t = datetime(year, month, day, hour, minute, second)"));
1517 assert!(labels.contains(&"t = datetime(serialDateNumbers, \"ConvertFrom\", \"datenum\")"));
1518
1519 assert_eq!(DATETIME_YEAR_DESCRIPTOR.signatures[0].label, "X = year(t)");
1520 assert_eq!(
1521 DATETIME_SUBSREF_DESCRIPTOR.signatures[0].label,
1522 "out = datetime.subsref(obj, kind, payload)"
1523 );
1524 assert_eq!(
1525 DATETIME_BINARY_DESCRIPTOR.signatures[0].label,
1526 "out = datetime.op(lhs, rhs)"
1527 );
1528 }
1529
1530 #[test]
1531 fn datetime_builds_from_components() {
1532 let value = run_datetime(vec![Value::Num(2024.0), Value::Num(3.0), Value::Num(14.0)]);
1533 let object = as_datetime(value);
1534 assert_eq!(object.class_name, DATETIME_CLASS);
1535 assert_eq!(format_for_object(&object), DEFAULT_DATE_FORMAT);
1536 let serials = serial_tensor_for_object(&object).expect("serials");
1537 assert_eq!(serials.data.len(), 1);
1538 let year =
1539 futures::executor::block_on(year_builtin(Value::Object(object.clone()))).expect("year");
1540 assert_eq!(year, Value::Num(2024.0));
1541 }
1542
1543 #[test]
1544 fn datetime_builds_arrays_from_component_vectors() {
1545 let years = Value::Tensor(Tensor::new(vec![2024.0, 2025.0], vec![1, 2]).unwrap());
1546 let months = Value::Tensor(Tensor::new(vec![1.0, 6.0], vec![1, 2]).unwrap());
1547 let days = Value::Tensor(Tensor::new(vec![15.0, 20.0], vec![1, 2]).unwrap());
1548 let value = run_datetime(vec![years, months, days]);
1549 let object = as_datetime(value.clone());
1550 let serials = serial_tensor_for_object(&object).expect("serials");
1551 assert_eq!(serials.shape, vec![1, 2]);
1552 let rendered = datetime_display_text(&value)
1553 .expect("display")
1554 .expect("datetime text");
1555 assert!(rendered.contains("15-Jan-2024"));
1556 assert!(rendered.contains("20-Jun-2025"));
1557 }
1558
1559 #[test]
1560 fn datetime_parses_text_and_converts_to_strings() {
1561 let value = run_datetime(vec![Value::String("2024-03-14 09:26:53".to_string())]);
1562 let rendered = datetime_string_array(&value)
1563 .expect("string array")
1564 .expect("datetime strings");
1565 assert_eq!(rendered.data, vec!["14-Mar-2024 09:26:53".to_string()]);
1566 }
1567
1568 #[test]
1569 fn datetime_supports_format_assignment() {
1570 let value = run_datetime(vec![Value::Num(2024.0), Value::Num(3.0), Value::Num(14.0)]);
1571 let updated = futures::executor::block_on(datetime_subsasgn(
1572 value,
1573 ".".to_string(),
1574 Value::String(FORMAT_FIELD.to_string()),
1575 Value::String("yyyy-MM-dd".to_string()),
1576 ))
1577 .expect("subsasgn");
1578 let rendered = datetime_display_text(&updated)
1579 .expect("display")
1580 .expect("datetime text");
1581 assert_eq!(rendered, "2024-03-14");
1582 }
1583
1584 #[test]
1585 fn datetime_supports_indexing_and_comparison() {
1586 let years = Value::Tensor(Tensor::new(vec![2024.0, 2025.0], vec![1, 2]).unwrap());
1587 let months = Value::Tensor(Tensor::new(vec![1.0, 6.0], vec![1, 2]).unwrap());
1588 let days = Value::Tensor(Tensor::new(vec![15.0, 20.0], vec![1, 2]).unwrap());
1589 let value = run_datetime(vec![years, months, days]);
1590 let payload =
1591 Value::Cell(runmat_builtins::CellArray::new(vec![Value::Num(2.0)], 1, 1).unwrap());
1592 let indexed =
1593 futures::executor::block_on(datetime_subsref(value.clone(), "()".to_string(), payload))
1594 .expect("subsref");
1595 let year = futures::executor::block_on(year_builtin(indexed)).expect("year");
1596 assert_eq!(year, Value::Num(2025.0));
1597
1598 let lhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(1.0)]);
1599 let rhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(2.0)]);
1600 let cmp = futures::executor::block_on(datetime_lt(lhs, rhs)).expect("lt");
1601 assert_eq!(cmp, Value::Num(1.0));
1602 }
1603
1604 #[test]
1605 fn datetime_and_duration_interoperate() {
1606 let lhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(1.0)]);
1607 let rhs = run_datetime(vec![Value::Num(2024.0), Value::Num(1.0), Value::Num(2.0)]);
1608 let delta = futures::executor::block_on(datetime_minus(rhs.clone(), lhs.clone()))
1609 .expect("datetime minus datetime");
1610 assert_eq!(delta, Value::Num(1.0));
1611
1612 let duration = crate::builtins::duration::duration_object_from_days_tensor(
1613 Tensor::new(vec![1.0], vec![1, 1]).unwrap(),
1614 crate::builtins::duration::DEFAULT_DURATION_FORMAT,
1615 )
1616 .expect("duration");
1617
1618 let round_trip = futures::executor::block_on(datetime_plus(lhs.clone(), duration.clone()))
1619 .expect("plus");
1620 let round_trip_text = datetime_display_text(&round_trip)
1621 .expect("datetime display")
1622 .expect("datetime text");
1623 assert_eq!(round_trip_text, "02-Jan-2024");
1624
1625 let restored =
1626 futures::executor::block_on(datetime_minus(rhs, duration)).expect("minus duration");
1627 let restored_text = datetime_display_text(&restored)
1628 .expect("datetime display")
1629 .expect("datetime text");
1630 assert_eq!(restored_text, "01-Jan-2024");
1631 }
1632}