1mod command;
6mod file_control;
7mod maps;
8mod program;
9mod run;
10mod services;
11
12pub use command::{Assign, Cics, CicsCommand, Control, Datum, FileControl, FileOptions, Handles, Opt, Record, Resp, Sink, Transfer};
13pub use run::{
14 At, CicsHost, EIBAID, EIBCALEN, EIBCPOSN, EIBDATE, EIBFN, EIBRESP, EIBRESP2, EIBRSRCE, EIBTASKN, EIBTIME, EIBTRMID,
15 EIBTRNID, Flow, Handler, Handlers, begin_command, begin_task, bytes, in_task, ok, raise, run, unsupported,
16};
17pub use run::{AbendExit, ExitTarget};
18pub use program::{abend_exit, enter_exit_program, level_ended};
19
20use crate::files::{Dd, Format, KeySpan, Keying};
21use crate::calendar::{civil, EPOCH_1900_TO_1970_MILLIS, EPOCH_1900_TO_1970_SECONDS, SECONDS_PER_DAY};
22pub use crate::terminal::Terminal;
23use std::collections::{BTreeMap, HashMap};
24use std::fs::OpenOptions;
25use std::io::Write;
26use std::path::PathBuf;
27
28pub const EIB_LEN: usize = 85;
30
31macro_rules! conditions {
32 ($($name:ident)*) => {
33 #[allow(clippy::upper_case_acronyms)]
35 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36 pub enum Condition {
37 $($name,)*
38 }
39
40 impl Condition {
41 pub const ALL: &[Self] = &[$(Self::$name,)*];
42
43 pub fn name(self) -> &'static str {
44 match self {
45 $(Self::$name => stringify!($name),)*
46 }
47 }
48 }
49 };
50}
51
52conditions! {
53 NORMAL ERROR RDATT WRBRK EOF EODS EOC INBFMH ENDINPT NONVAL NOSTART TERMIDERR FILENOTFOUND
54 NOTFND DUPREC DUPKEY INVREQ IOERR NOSPACE NOTOPEN ENDFILE ILLOGIC LENGERR QZERO SIGNAL QBUSY
55 ITEMERR PGMIDERR TRANSIDERR ENDDATA INVTSREQ EXPIRED RETPAGE RTEFAIL RTESOME TSIOERR MAPFAIL
56 INVERRTERM INVMPSZ IGREQID OVERFLOW INVLDC NOSTG JIDERR QIDERR NOJBUFSP DSSTAT SELNERR FUNCERR
57 UNEXPIN NOPASSBKRD NOPASSBKWR SEGIDERR SYSIDERR ISCINVREQ ENQBUSY ENVDEFERR IGREQCD SESSIONERR
58 SYSBUSY SESSBUSY NOTALLOC CBIDERR INVEXITREQ INVPARTNSET INVPARTN PARTNFAIL USERIDERR NOTAUTH
59 VOLIDERR SUPPRESSED RESIDERR NOSPOOL TERMERR ROLLEDBACK END DISABLED ALLOCERR STRELERR OPENERR
60 SPOLBUSY SPOLERR NODEIDERR TASKIDERR TCIDERR DSNNOTFOUND LOADING MODELIDERR OUTDESCRERR
61 PARTNERIDERR PROFILEIDERR NETNAMEIDERR LOCKED RECORDBUSY UOWNOTFOUND UOWLNOTFOUND LINKABEND
62 CHANGED PROCESSBUSY ACTIVITYBUSY PROCESSERR ACTIVITYERR CONTAINERERR EVENTERR TOKENERR
63 NOTFINISHED POOLERR TIMERERR SYMBOLERR TEMPLATERR NOTSUPERUSER CSDERR DUPRES RESUNAVAIL
64 CHANNELERR CCSIDERR TIMEDOUT CODEPAGEERR INCOMPLETE APPNOTFOUND BUSY
65}
66
67const ALIASES: &[(&str, Condition)] = &[("DSIDERR", Condition::FILENOTFOUND)];
69
70const DEFAULT_ABENDS: &[(Condition, &str)] = &[
73 (Condition::NOTFND, "AEIM"),
74 (Condition::DUPREC, "AEIN"),
75 (Condition::DUPKEY, "AEIO"),
76 (Condition::IOERR, "AEIQ"),
77 (Condition::NOSPACE, "AEIR"),
78 (Condition::NOTOPEN, "AEIS"),
79 (Condition::ENDFILE, "AEIT"),
80 (Condition::ILLOGIC, "AEIU"),
81 (Condition::LENGERR, "AEIV"),
82 (Condition::QZERO, "AEIW"),
83 (Condition::ITEMERR, "AEIZ"),
84 (Condition::PGMIDERR, "AEI0"),
85 (Condition::TRANSIDERR, "AEI1"),
86 (Condition::ENDDATA, "AEI2"),
87 (Condition::INVTSREQ, "AEI3"),
88 (Condition::EXPIRED, "AEI4"),
89 (Condition::TSIOERR, "AEI8"),
90 (Condition::MAPFAIL, "AEI9"),
91 (Condition::ERROR, "AEIA"),
92 (Condition::EOF, "AEID"),
93 (Condition::EODS, "AEIE"),
94 (Condition::INBFMH, "AEIG"),
95 (Condition::ENDINPT, "AEIH"),
96 (Condition::NONVAL, "AEII"),
97 (Condition::NOSTART, "AEIJ"),
98 (Condition::TERMIDERR, "AEIK"),
99 (Condition::FILENOTFOUND, "AEIL"),
100 (Condition::DISABLED, "AEXL"),
101 (Condition::ROLLEDBACK, "AEXJ"),
102 (Condition::LOCKED, "AEX8"),
103 (Condition::RECORDBUSY, "AEX9"),
104 (Condition::QIDERR, "AEYH"),
105 (Condition::SYSIDERR, "AEYQ"),
106 (Condition::NOTAUTH, "AEY7"),
107 (Condition::USERIDERR, "AEYX"),
108 (Condition::CONTAINERERR, "AEZJ"),
109 (Condition::CHANNELERR, "AEZV"),
110];
111
112impl Condition {
113 pub fn from_name(name: &str) -> Option<Self> {
115 let alias = ALIASES.iter().find(|(a, _)| a.eq_ignore_ascii_case(name)).map(|&(_, c)| c);
116 alias.or_else(|| Self::ALL.iter().copied().find(|c| c.name().eq_ignore_ascii_case(name)))
117 }
118
119 pub fn resp(self) -> i32 {
121 crate::cics_tables::resp(self.name()).unwrap_or_else(|| panic!("DFHRESP has no {}", self.name()))
122 }
123
124 pub fn default_abend(self) -> &'static str {
125 DEFAULT_ABENDS.iter().find(|&&(c, _)| c == self).map_or("AEIP", |&(_, abend)| abend)
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
131pub enum DataSet {
132 Ksds { key: KeySpan },
133 Rrds,
134}
135
136#[derive(Clone, Debug, PartialEq, Eq)]
138pub struct FileDef {
139 pub dd: Dd,
140 pub data_set: DataSet,
141 pub record_len: usize,
142}
143
144impl FileDef {
145 pub fn keying(&self) -> Keying {
147 match &self.data_set {
148 DataSet::Ksds { key } => Keying::Indexed { prime: *key, alternates: vec![] },
149 DataSet::Rrds => Keying::Relative,
150 }
151 }
152
153 pub fn format(&self) -> Format {
154 self.dd.format.unwrap_or(Format::Fixed)
155 }
156}
157
158pub fn parse_file(spec: &str) -> Result<(String, FileDef), String> {
161 let parts: Vec<&str> = spec.split(',').collect();
162 if parts.len() < 3 {
163 return Err(format!("{spec}: expected NAME=path,KIND,…"));
164 }
165
166 let (name, path) = parts[0]
167 .split_once('=')
168 .ok_or_else(|| format!("{spec}: expected NAME=path"))?;
169 let name = name.to_ascii_uppercase();
170 let kind = parts[1].to_ascii_uppercase();
171
172 let mut key: Option<KeySpan> = None;
173 let mut record_len: Option<usize> = None;
174 let mut format: Option<Format> = None;
175
176 for part in &parts[2..] {
177 let part = part.trim();
178 if let Some(val) = part.strip_prefix("key=") {
179 let (off, len) = val
180 .split_once(':')
181 .ok_or_else(|| format!("{spec}: bad key spec {val}"))?;
182 let off: usize = off.parse().map_err(|_| format!("{spec}: bad key offset {off}"))?;
183 let len: usize = len.parse().map_err(|_| format!("{spec}: bad key length {len}"))?;
184 key = Some(KeySpan { offset: off, len });
185 } else if let Some(val) = part.strip_prefix("len=") {
186 record_len = Some(
187 val.parse()
188 .map_err(|_| format!("{spec}: bad record length {val}"))?,
189 );
190 } else if let Some(f) = Format::from_keyword(part) {
191 format = Some(f);
192 } else {
193 return Err(format!("{spec}: unknown field {part}"));
194 }
195 }
196
197 let data_set = match kind.as_str() {
198 "KSDS" => {
199 let key = key.ok_or_else(|| format!("{spec}: KSDS requires key=OFFSET:LENGTH"))?;
200 DataSet::Ksds { key }
201 }
202 "RRDS" => DataSet::Rrds,
203 _ => return Err(format!("{spec}: unknown data set kind {kind}")),
204 };
205
206 let record_len = record_len.ok_or_else(|| format!("{spec}: missing len=RECLEN"))?;
207 let dd = Dd { path: PathBuf::from(path), format, append: false };
208 Ok((name, FileDef { dd, data_set, record_len }))
209}
210
211#[derive(Clone, Debug, Default, PartialEq, Eq)]
213pub struct TsQueue {
214 pub items: Vec<Vec<u8>>,
215 pub next: usize,
216}
217
218#[derive(Clone, Debug, Default, PartialEq, Eq)]
221pub struct Browse {
222 pub at: Vec<u8>,
223 pub inclusive: bool,
224}
225
226#[derive(Debug, Default)]
228pub struct Task {
229 pub transid: String,
230 pub termid: String,
231 pub userid: String,
232 pub applid: String,
233 pub sysid: String,
234 pub number: u32,
235 pub commarea: Option<Vec<u8>>,
237 pub files: HashMap<String, FileDef>,
238 pub ts: BTreeMap<String, TsQueue>,
239 pub td: BTreeMap<String, Vec<Vec<u8>>>,
240 pub td_files: HashMap<String, PathBuf>,
242 pub held: HashMap<String, Vec<u8>>,
244 pub browses: HashMap<(String, i64), Browse>,
246 pub terminal: Option<Box<dyn Terminal>>,
247 pub initial_aid: Option<u8>,
249 pub mapsets: HashMap<String, crate::bms::Mapset>,
251 pub next_transid: Option<String>,
253 pub returned_commarea: Option<Vec<u8>>,
254 pub abcode: Option<String>,
256 pub cancelling: bool,
258 pub activations: u64,
260 pub links: u32,
263 pub ending_level: bool,
266}
267
268impl Task {
269 pub fn next_activation(&mut self) -> u64 {
271 self.activations += 1;
272 self.activations
273 }
274
275 pub fn writeq_ts(
278 &mut self,
279 queue: &str,
280 rewrite: Option<usize>,
281 data: &[u8],
282 ) -> Result<usize, Condition> {
283 let queue = queue.trim_end();
284 match rewrite {
285 None => {
286 let q = self
287 .ts
288 .entry(queue.to_string())
289 .or_default();
290 q.items.push(data.to_vec());
291 Ok(q.items.len())
292 }
293 Some(item) => {
294 let q = self.ts.get_mut(queue).ok_or(Condition::QIDERR)?;
295 if item == 0 || item > q.items.len() {
296 return Err(Condition::ITEMERR);
297 }
298 q.items[item - 1] = data.to_vec();
299 Ok(item)
300 }
301 }
302 }
303
304 pub fn readq_ts(
306 &mut self,
307 queue: &str,
308 item: Option<usize>,
309 ) -> Result<(Vec<u8>, usize), Condition> {
310 let queue = queue.trim_end();
311 let q = self.ts.get_mut(queue).ok_or(Condition::QIDERR)?;
312 let item_num = match item {
313 Some(n) => {
314 if n == 0 || n > q.items.len() {
315 return Err(Condition::ITEMERR);
316 }
317 n
318 }
319 None => {
320 let n = if q.next == 0 { 1 } else { q.next + 1 };
321 if n > q.items.len() {
322 return Err(Condition::ITEMERR);
323 }
324 n
325 }
326 };
327 let data = q.items[item_num - 1].clone();
328 q.next = item_num;
329 Ok((data, q.items.len()))
330 }
331
332 pub fn deleteq_ts(&mut self, queue: &str) -> Result<(), Condition> {
334 let queue = queue.trim_end();
335 self.ts.remove(queue).map(|_| ()).ok_or(Condition::QIDERR)
336 }
337
338 pub fn writeq_td(&mut self, queue: &str, data: &[u8]) {
340 let queue = queue.trim_end();
341 self.td.entry(queue.to_string()).or_default().push(data.to_vec());
342 }
343
344 pub fn readq_td(&mut self, queue: &str) -> Result<Vec<u8>, Condition> {
346 let queue = queue.trim_end();
347 let items = self.td.get_mut(queue).ok_or(Condition::QZERO)?;
348 if items.is_empty() {
349 return Err(Condition::QZERO);
350 }
351 Ok(items.remove(0))
352 }
353
354 pub fn deleteq_td(&mut self, queue: &str) {
356 let queue = queue.trim_end();
357 if let Some(items) = self.td.get_mut(queue) {
358 items.clear();
359 }
360 }
361
362 pub fn flush_td(&mut self, page: &zarch::ebcdic::CodePage) -> std::io::Result<()> {
365 let queues: Vec<String> = self.td_files.keys().cloned().collect();
366 for queue in &queues {
367 let Some(path) = self.td_files.get(queue) else { continue };
368 let items: Vec<Vec<u8>> = self
369 .td
370 .get(queue)
371 .cloned()
372 .unwrap_or_default();
373 if items.is_empty() {
374 continue;
375 }
376 let mut file = OpenOptions::new().create(true).append(true).open(path)?;
377 for item in &items {
378 let text = page.decode(item);
379 let line = format!("{}\n", text.trim_end());
380 file.write_all(line.as_bytes())?;
381 }
382 if let Some(q) = self.td.get_mut(queue) {
383 q.clear();
384 }
385 }
386 Ok(())
387 }
388}
389
390pub fn abstime(seconds: i64, hundredths: u32) -> i64 {
392 EPOCH_1900_TO_1970_MILLIS + seconds * 1000 + hundredths as i64 * 10
393}
394
395pub fn unix_seconds(abstime: i64) -> i64 {
397 (abstime - EPOCH_1900_TO_1970_MILLIS).div_euclid(1000)
398}
399
400pub fn eib_date(abstime: i64) -> i64 {
402 let secs = unix_seconds(abstime);
403 let date = civil(secs);
404 let c = if date.year >= 2000 { 1 } else { 0 };
405 let yy = date.year % 100;
406 c * 100_000 + yy * 1_000 + i64::from(date.day_of_year)
407}
408
409pub fn eib_time(abstime: i64) -> i64 {
411 let secs = unix_seconds(abstime);
412 let time = civil(secs);
413 i64::from(time.hour) * 10_000 + i64::from(time.minute) * 100 + i64::from(time.second)
414}
415
416#[derive(Clone, Debug, PartialEq, Eq)]
418pub enum FormatValue {
419 Text(String),
420 Number(i64),
421}
422
423pub fn format_time(abstime: i64, option: &str, datesep: Option<char>, timesep: Option<char>) -> Option<FormatValue> {
426 let secs = unix_seconds(abstime);
427 let c = civil(secs);
428 let (year, month, day, hour, minute, second, doy) = (c.year, c.month, c.day, c.hour, c.minute, c.second, c.day_of_year);
429
430 let yy = format!("{:02}", year % 100);
431 let yyyy = format!("{:04}", year);
432 let mm = format!("{:02}", month);
433 let dd = format!("{:02}", day);
434 let hh = format!("{:02}", hour);
435 let mi = format!("{:02}", minute);
436 let ss = format!("{:02}", second);
437 let ddd = format!("{:03}", doy);
438
439 let join = |parts: &[&str], sep: Option<char>| match sep {
440 Some(c) => {
441 let mut s = String::new();
442 for (i, p) in parts.iter().enumerate() {
443 if i > 0 {
444 s.push(c);
445 }
446 s.push_str(p);
447 }
448 s
449 }
450 None => parts.concat(),
451 };
452
453 let option = option.to_ascii_uppercase();
454
455 match option.as_str() {
456 "YYYYMMDD" => Some(FormatValue::Text(join(&[&yyyy, &mm, &dd], datesep))),
457 "YYMMDD" => Some(FormatValue::Text(join(&[&yy, &mm, &dd], datesep))),
458 "YYDDMM" => Some(FormatValue::Text(join(&[&yy, &dd, &mm], datesep))),
459 "YYYYDDMM" => Some(FormatValue::Text(join(&[&yyyy, &dd, &mm], datesep))),
460 "DDMMYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yy], datesep))),
461 "DDMMYYYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yyyy], datesep))),
462 "MMDDYY" | "DATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yy], datesep))),
463 "MMDDYYYY" | "FULLDATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yyyy], datesep))),
464 "YYDDD" => Some(FormatValue::Text(join(&[&yy, &ddd], datesep))),
465 "YYYYDDD" => Some(FormatValue::Text(join(&[&yyyy, &ddd], datesep))),
466 "DATEFORM" => Some(FormatValue::Text("MMDDYY".to_string())),
467 "TIME" => Some(FormatValue::Text(join(&[&hh, &mi, &ss], timesep))),
468 "DAYCOUNT" => Some(FormatValue::Number((secs + EPOCH_1900_TO_1970_SECONDS) / SECONDS_PER_DAY)),
469 "DAYOFWEEK" => Some(FormatValue::Number(c.cics_weekday())),
470 "DAYOFMONTH" => Some(FormatValue::Number(day as i64)),
471 "MONTHOFYEAR" => Some(FormatValue::Number(month as i64)),
472 "YEAR" => Some(FormatValue::Number(year)),
473 "MILLISECONDS" => Some(FormatValue::Number(abstime.rem_euclid(1000))),
474 _ => None,
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn every_condition_is_a_dfhresp_row_with_its_number() {
484 for &c in Condition::ALL {
485 assert_eq!(Some(c.resp()), crate::cics_tables::resp(c.name()), "{c:?}");
486 assert_eq!(Condition::from_name(c.name()), Some(c));
487 }
488 let rows: Vec<&str> = crate::cics_tables::resp_table().iter().map(|r| r.condition).collect();
489 let names: Vec<&str> = Condition::ALL.iter().map(|c| c.name()).collect();
490 assert_eq!(names, rows, "one variant per DFHRESP row, in its order");
491 }
492
493 #[test]
494 fn every_default_abend_is_for_a_dfhresp_condition_and_listed_once() {
495 for (i, &(c, abend)) in DEFAULT_ABENDS.iter().enumerate() {
496 assert!(crate::cics_tables::resp(c.name()).is_some(), "{c:?}");
497 assert!(DEFAULT_ABENDS[..i].iter().all(|&(d, _)| d != c), "{c:?} is listed twice");
498 assert_eq!(c.default_abend(), abend);
499 }
500 assert_eq!(DEFAULT_ABENDS.len(), 37);
501 let codes = [Condition::QIDERR, Condition::ROLLEDBACK, Condition::FILENOTFOUND, Condition::INVREQ, Condition::INVMPSZ].map(Condition::default_abend);
502 assert_eq!(codes, ["AEYH", "AEXJ", "AEIL", "AEIP", "AEIP"]);
503 }
504
505 #[test]
506 fn dsiderr_is_filenotfound_by_its_older_name() {
507 let c = Condition::from_name("dsiderr").unwrap();
508 assert_eq!((c, c.name(), c.resp()), (Condition::FILENOTFOUND, "FILENOTFOUND", 12));
509 assert_eq!(Condition::from_name("NOSUCH"), None);
510 }
511
512 #[test]
513 fn parse_file_ksds() {
514 let (name, def) = parse_file("MYFILE=/tmp/data,KSDS,key=0:8,len=80").unwrap();
515 assert_eq!(name, "MYFILE");
516 assert_eq!(def.record_len, 80);
517 assert!(matches!(&def.data_set, DataSet::Ksds { key } if key.offset == 0 && key.len == 8));
518 }
519
520 #[test]
521 fn parse_file_rrds_text() {
522 let (name, def) = parse_file("myfile=/tmp/data,rrds,len=120,text").unwrap();
523 assert_eq!(name, "MYFILE");
524 assert_eq!(def.record_len, 120);
525 assert!(matches!(def.data_set, DataSet::Rrds));
526 assert_eq!(def.dd.format, Some(Format::Text));
527 }
528
529 #[test]
530 fn parse_file_errors() {
531 assert!(parse_file("F=/tmp/d,KSDS,len=80").is_err());
532 assert!(parse_file("F=/tmp/d,RRDS,len=abc").is_err());
533 assert!(parse_file("F=/tmp/d,FOO,len=80").is_err());
534 }
535
536 #[test]
537 fn ts_queue() {
538 let mut task = Task::default();
539 task.writeq_ts("Q1", None, b"one").unwrap();
540 task.writeq_ts("Q1", None, b"two").unwrap();
541 task.writeq_ts("Q1", None, b"three").unwrap();
542
543 let (data, count) = task.readq_ts("Q1", Some(2)).unwrap();
544 assert_eq!(data, b"two");
545 assert_eq!(count, 3);
546
547 let (data, count) = task.readq_ts("Q1", None).unwrap();
548 assert_eq!(data, b"three");
549 assert_eq!(count, 3);
550
551 assert_eq!(task.readq_ts("Q1", None), Err(Condition::ITEMERR));
552
553 task.writeq_ts("Q1", Some(1), b"ONE").unwrap();
554 let (data, _) = task.readq_ts("Q1", Some(1)).unwrap();
555 assert_eq!(data, b"ONE");
556
557 assert_eq!(task.writeq_ts("Q1", Some(9), b"x"), Err(Condition::ITEMERR));
558 assert_eq!(task.readq_ts("NOPE", None), Err(Condition::QIDERR));
559
560 task.deleteq_ts("Q1").unwrap();
561 assert_eq!(task.readq_ts("Q1", None), Err(Condition::QIDERR));
562 }
563
564 #[test]
565 fn td_queue() {
566 let mut task = Task::default();
567 task.writeq_td("Q1", b"first");
568 task.writeq_td("Q1", b"second");
569 task.writeq_td("Q1", b"third");
570
571 assert_eq!(task.readq_td("Q1").unwrap(), b"first");
572 assert_eq!(task.readq_td("Q1").unwrap(), b"second");
573 assert_eq!(task.readq_td("Q1").unwrap(), b"third");
574 assert_eq!(task.readq_td("Q1"), Err(Condition::QZERO));
575 }
576
577 #[test]
578 fn abstime_and_unix() {
579 assert_eq!(abstime(0, 0), 2_208_988_800_000);
580 assert_eq!(abstime(1, 50), 2_208_988_801_500);
581 assert_eq!(unix_seconds(abstime(1, 50)), 1);
582 assert_eq!(unix_seconds(abstime(0, 0)), 0);
583 }
584
585 #[test]
586 fn eib_date_and_time() {
587 let at = abstime(1_790_514_309, 0);
589 assert_eq!(eib_date(at), 126_270);
590 assert_eq!(eib_time(at), 130_509);
591
592 let at = abstime(946_598_400, 0);
594 assert_eq!(eib_date(at), 99_365);
595 assert_eq!(eib_time(at), 0);
596 }
597
598 #[test]
599 fn format_time_options() {
600 let at = abstime(1_790_514_309, 25);
601
602 assert_eq!(format_time(at, "YYYYMMDD", Some('/'), None), Some(FormatValue::Text("2026/09/27".into())));
603 assert_eq!(format_time(at, "MMDDYY", None, None), Some(FormatValue::Text("092726".into())));
604 assert_eq!(format_time(at, "YYDDD", Some('-'), None), Some(FormatValue::Text("26-270".into())));
605 assert_eq!(format_time(at, "TIME", None, Some(':')), Some(FormatValue::Text("13:05:09".into())));
606 assert_eq!(format_time(at, "DAYOFWEEK", None, None), Some(FormatValue::Number(0)));
607 assert_eq!(format_time(at, "DAYOFMONTH", None, None), Some(FormatValue::Number(27)));
608 assert_eq!(format_time(at, "MONTHOFYEAR", None, None), Some(FormatValue::Number(9)));
609 assert_eq!(format_time(at, "YEAR", None, None), Some(FormatValue::Number(2026)));
610 assert_eq!(format_time(at, "MILLISECONDS", None, None), Some(FormatValue::Number(250)));
611 assert_eq!(format_time(at, "DAYCOUNT", None, None), Some(FormatValue::Number(46_290)));
612 assert_eq!(format_time(at, "STRINGFORMAT", None, None), None);
613 }
614}