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, PartialOrd, Ord)]
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::INVMPSZ, "AEYB"),
75 (Condition::DUPREC, "AEIN"),
76 (Condition::DUPKEY, "AEIO"),
77 (Condition::IOERR, "AEIQ"),
78 (Condition::NOSPACE, "AEIR"),
79 (Condition::NOTOPEN, "AEIS"),
80 (Condition::ENDFILE, "AEIT"),
81 (Condition::ILLOGIC, "AEIU"),
82 (Condition::LENGERR, "AEIV"),
83 (Condition::QZERO, "AEIW"),
84 (Condition::ITEMERR, "AEIZ"),
85 (Condition::PGMIDERR, "AEI0"),
86 (Condition::TRANSIDERR, "AEI1"),
87 (Condition::ENDDATA, "AEI2"),
88 (Condition::INVTSREQ, "AEI3"),
89 (Condition::EXPIRED, "AEI4"),
90 (Condition::TSIOERR, "AEI8"),
91 (Condition::MAPFAIL, "AEI9"),
92 (Condition::ERROR, "AEIA"),
93 (Condition::EOF, "AEID"),
94 (Condition::EODS, "AEIE"),
95 (Condition::INBFMH, "AEIG"),
96 (Condition::ENDINPT, "AEIH"),
97 (Condition::NONVAL, "AEII"),
98 (Condition::NOSTART, "AEIJ"),
99 (Condition::TERMIDERR, "AEIK"),
100 (Condition::FILENOTFOUND, "AEIL"),
101 (Condition::DISABLED, "AEXL"),
102 (Condition::ROLLEDBACK, "AEXJ"),
103 (Condition::LOCKED, "AEX8"),
104 (Condition::RECORDBUSY, "AEX9"),
105 (Condition::QIDERR, "AEYH"),
106 (Condition::SYSIDERR, "AEYQ"),
107 (Condition::NOTAUTH, "AEY7"),
108 (Condition::USERIDERR, "AEYX"),
109 (Condition::CONTAINERERR, "AEZJ"),
110 (Condition::CHANNELERR, "AEZV"),
111];
112
113impl Condition {
114 pub fn from_name(name: &str) -> Option<Self> {
116 let alias = ALIASES.iter().find(|(a, _)| a.eq_ignore_ascii_case(name)).map(|&(_, c)| c);
117 alias.or_else(|| Self::ALL.iter().copied().find(|c| c.name().eq_ignore_ascii_case(name)))
118 }
119
120 pub fn resp(self) -> i32 {
122 crate::cics_tables::resp(self.name()).unwrap_or_else(|| panic!("DFHRESP has no {}", self.name()))
123 }
124
125 pub fn default_abend(self) -> &'static str {
126 DEFAULT_ABENDS.iter().find(|&&(c, _)| c == self).map_or("AEIP", |&(_, abend)| abend)
127 }
128}
129
130#[derive(Clone, Debug, PartialEq, Eq)]
132pub enum DataSet {
133 Ksds { key: KeySpan },
134 Rrds,
135}
136
137#[derive(Clone, Debug, PartialEq, Eq)]
139pub struct FileDef {
140 pub dd: Dd,
141 pub data_set: DataSet,
142 pub record_len: usize,
143}
144
145impl FileDef {
146 pub fn keying(&self) -> Keying {
148 match &self.data_set {
149 DataSet::Ksds { key } => Keying::Indexed { prime: key.clone(), alternates: vec![] },
150 DataSet::Rrds => Keying::Relative,
151 }
152 }
153
154 pub fn format(&self) -> Format {
155 self.dd.format.unwrap_or(Format::Fixed)
156 }
157}
158
159pub fn parse_file(spec: &str) -> Result<(String, FileDef), String> {
162 let parts: Vec<&str> = spec.split(',').collect();
163 if parts.len() < 3 {
164 return Err(format!("{spec}: expected NAME=path,KIND,…"));
165 }
166
167 let (name, path) = parts[0]
168 .split_once('=')
169 .ok_or_else(|| format!("{spec}: expected NAME=path"))?;
170 let name = name.to_ascii_uppercase();
171 let kind = parts[1].to_ascii_uppercase();
172
173 let mut key: Option<KeySpan> = None;
174 let mut record_len: Option<usize> = None;
175 let mut format: Option<Format> = None;
176
177 for part in &parts[2..] {
178 let part = part.trim();
179 if let Some(val) = part.strip_prefix("key=") {
180 let (off, len) = val
181 .split_once(':')
182 .ok_or_else(|| format!("{spec}: bad key spec {val}"))?;
183 let off: usize = off.parse().map_err(|_| format!("{spec}: bad key offset {off}"))?;
184 let len: usize = len.parse().map_err(|_| format!("{spec}: bad key length {len}"))?;
185 key = Some(KeySpan::new(off, len));
186 } else if let Some(val) = part.strip_prefix("len=") {
187 record_len = Some(
188 val.parse()
189 .map_err(|_| format!("{spec}: bad record length {val}"))?,
190 );
191 } else if let Some(f) = Format::from_keyword(part) {
192 format = Some(f);
193 } else {
194 return Err(format!("{spec}: unknown field {part}"));
195 }
196 }
197
198 let data_set = match kind.as_str() {
199 "KSDS" => {
200 let key = key.ok_or_else(|| format!("{spec}: KSDS requires key=OFFSET:LENGTH"))?;
201 DataSet::Ksds { key }
202 }
203 "RRDS" => DataSet::Rrds,
204 _ => return Err(format!("{spec}: unknown data set kind {kind}")),
205 };
206
207 let record_len = record_len.ok_or_else(|| format!("{spec}: missing len=RECLEN"))?;
208 let dd = Dd { path: PathBuf::from(path), format, append: false };
209 Ok((name, FileDef { dd, data_set, record_len }))
210}
211
212#[derive(Clone, Debug, Default, PartialEq, Eq)]
214pub struct TsQueue {
215 pub items: Vec<Vec<u8>>,
216 pub next: usize,
217}
218
219#[derive(Clone, Debug, Default, PartialEq, Eq)]
222pub struct Browse {
223 pub at: Vec<u8>,
224 pub inclusive: bool,
225}
226
227#[derive(Debug, Default)]
229pub struct Task {
230 pub transid: String,
231 pub termid: String,
232 pub userid: String,
233 pub applid: String,
234 pub sysid: String,
235 pub number: u32,
236 pub commarea: Option<Vec<u8>>,
238 pub files: HashMap<String, FileDef>,
239 pub ts: BTreeMap<String, TsQueue>,
240 pub td: BTreeMap<String, Vec<Vec<u8>>>,
241 pub td_files: HashMap<String, PathBuf>,
243 pub held: HashMap<String, Vec<u8>>,
245 pub browses: HashMap<(String, i64), Browse>,
247 pub terminal: Option<Box<dyn Terminal>>,
248 pub initial_aid: Option<u8>,
250 pub mapsets: HashMap<String, crate::bms::Mapset>,
252 pub next_transid: Option<String>,
254 pub returned_commarea: Option<Vec<u8>>,
255 pub abcode: Option<String>,
257 pub cancelling: bool,
259 pub activations: u64,
261 pub links: u32,
264 pub ending_level: bool,
267}
268
269impl Task {
270 pub fn next_activation(&mut self) -> u64 {
272 self.activations += 1;
273 self.activations
274 }
275
276 pub fn writeq_ts(
279 &mut self,
280 queue: &str,
281 rewrite: Option<usize>,
282 data: &[u8],
283 ) -> Result<usize, Condition> {
284 let queue = queue.trim_end();
285 match rewrite {
286 None => {
287 let q = self
288 .ts
289 .entry(queue.to_string())
290 .or_default();
291 q.items.push(data.to_vec());
292 Ok(q.items.len())
293 }
294 Some(item) => {
295 let q = self.ts.get_mut(queue).ok_or(Condition::QIDERR)?;
296 if item == 0 || item > q.items.len() {
297 return Err(Condition::ITEMERR);
298 }
299 q.items[item - 1] = data.to_vec();
300 Ok(item)
301 }
302 }
303 }
304
305 pub fn readq_ts(
307 &mut self,
308 queue: &str,
309 item: Option<usize>,
310 ) -> Result<(Vec<u8>, usize), Condition> {
311 let queue = queue.trim_end();
312 let q = self.ts.get_mut(queue).ok_or(Condition::QIDERR)?;
313 let item_num = match item {
314 Some(n) => {
315 if n == 0 || n > q.items.len() {
316 return Err(Condition::ITEMERR);
317 }
318 n
319 }
320 None => {
321 let n = if q.next == 0 { 1 } else { q.next + 1 };
322 if n > q.items.len() {
323 return Err(Condition::ITEMERR);
324 }
325 n
326 }
327 };
328 let data = q.items[item_num - 1].clone();
329 q.next = item_num;
330 Ok((data, q.items.len()))
331 }
332
333 pub fn deleteq_ts(&mut self, queue: &str) -> Result<(), Condition> {
335 let queue = queue.trim_end();
336 self.ts.remove(queue).map(|_| ()).ok_or(Condition::QIDERR)
337 }
338
339 pub fn writeq_td(&mut self, queue: &str, data: &[u8]) {
341 let queue = queue.trim_end();
342 self.td.entry(queue.to_string()).or_default().push(data.to_vec());
343 }
344
345 pub fn readq_td(&mut self, queue: &str) -> Result<Vec<u8>, Condition> {
347 let queue = queue.trim_end();
348 let items = self.td.get_mut(queue).ok_or(Condition::QZERO)?;
349 if items.is_empty() {
350 return Err(Condition::QZERO);
351 }
352 Ok(items.remove(0))
353 }
354
355 pub fn deleteq_td(&mut self, queue: &str) {
357 let queue = queue.trim_end();
358 if let Some(items) = self.td.get_mut(queue) {
359 items.clear();
360 }
361 }
362
363 pub fn flush_td(&mut self, page: &zarch::ebcdic::CodePage) -> std::io::Result<()> {
366 let queues: Vec<String> = self.td_files.keys().cloned().collect();
367 for queue in &queues {
368 let Some(path) = self.td_files.get(queue) else { continue };
369 let items: Vec<Vec<u8>> = self
370 .td
371 .get(queue)
372 .cloned()
373 .unwrap_or_default();
374 if items.is_empty() {
375 continue;
376 }
377 let mut file = OpenOptions::new().create(true).append(true).open(path)?;
378 for item in &items {
379 let text = page.decode(item);
380 let line = format!("{}\n", text.trim_end());
381 file.write_all(line.as_bytes())?;
382 }
383 if let Some(q) = self.td.get_mut(queue) {
384 q.clear();
385 }
386 }
387 Ok(())
388 }
389}
390
391pub fn abstime(seconds: i64, hundredths: u32) -> i64 {
393 EPOCH_1900_TO_1970_MILLIS + seconds * 1000 + hundredths as i64 * 10
394}
395
396pub fn unix_seconds(abstime: i64) -> i64 {
398 (abstime - EPOCH_1900_TO_1970_MILLIS).div_euclid(1000)
399}
400
401pub fn eib_date(abstime: i64) -> i64 {
403 let secs = unix_seconds(abstime);
404 let date = civil(secs);
405 let c = if date.year >= 2000 { 1 } else { 0 };
406 let yy = date.year % 100;
407 c * 100_000 + yy * 1_000 + i64::from(date.day_of_year)
408}
409
410pub fn eib_time(abstime: i64) -> i64 {
412 let secs = unix_seconds(abstime);
413 let time = civil(secs);
414 i64::from(time.hour) * 10_000 + i64::from(time.minute) * 100 + i64::from(time.second)
415}
416
417#[derive(Clone, Debug, PartialEq, Eq)]
419pub enum FormatValue {
420 Text(String),
421 Number(i64),
422}
423
424pub fn format_time(abstime: i64, option: &str, datesep: Option<char>, timesep: Option<char>) -> Option<FormatValue> {
427 let secs = unix_seconds(abstime);
428 let c = civil(secs);
429 let (year, month, day, hour, minute, second, doy) = (c.year, c.month, c.day, c.hour, c.minute, c.second, c.day_of_year);
430
431 let yy = format!("{:02}", year % 100);
432 let yyyy = format!("{:04}", year);
433 let mm = format!("{:02}", month);
434 let dd = format!("{:02}", day);
435 let hh = format!("{:02}", hour);
436 let mi = format!("{:02}", minute);
437 let ss = format!("{:02}", second);
438 let ddd = format!("{:03}", doy);
439
440 let join = |parts: &[&str], sep: Option<char>| match sep {
441 Some(c) => {
442 let mut s = String::new();
443 for (i, p) in parts.iter().enumerate() {
444 if i > 0 {
445 s.push(c);
446 }
447 s.push_str(p);
448 }
449 s
450 }
451 None => parts.concat(),
452 };
453
454 let option = option.to_ascii_uppercase();
455
456 match option.as_str() {
457 "YYYYMMDD" => Some(FormatValue::Text(join(&[&yyyy, &mm, &dd], datesep))),
458 "YYMMDD" => Some(FormatValue::Text(join(&[&yy, &mm, &dd], datesep))),
459 "YYDDMM" => Some(FormatValue::Text(join(&[&yy, &dd, &mm], datesep))),
460 "YYYYDDMM" => Some(FormatValue::Text(join(&[&yyyy, &dd, &mm], datesep))),
461 "DDMMYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yy], datesep))),
462 "DDMMYYYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yyyy], datesep))),
463 "MMDDYY" | "DATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yy], datesep))),
464 "MMDDYYYY" | "FULLDATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yyyy], datesep))),
465 "YYDDD" => Some(FormatValue::Text(join(&[&yy, &ddd], datesep))),
466 "YYYYDDD" => Some(FormatValue::Text(join(&[&yyyy, &ddd], datesep))),
467 "DATEFORM" => Some(FormatValue::Text("MMDDYY".to_string())),
468 "TIME" => Some(FormatValue::Text(join(&[&hh, &mi, &ss], timesep))),
469 "DAYCOUNT" => Some(FormatValue::Number((secs + EPOCH_1900_TO_1970_SECONDS) / SECONDS_PER_DAY)),
470 "DAYOFWEEK" => Some(FormatValue::Number(c.cics_weekday())),
471 "DAYOFMONTH" => Some(FormatValue::Number(day as i64)),
472 "MONTHOFYEAR" => Some(FormatValue::Number(month as i64)),
473 "YEAR" => Some(FormatValue::Number(year)),
474 "MILLISECONDS" => Some(FormatValue::Number(abstime.rem_euclid(1000))),
475 _ => None,
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn every_condition_is_a_dfhresp_row_with_its_number() {
485 for &c in Condition::ALL {
486 assert_eq!(Some(c.resp()), crate::cics_tables::resp(c.name()), "{c:?}");
487 assert_eq!(Condition::from_name(c.name()), Some(c));
488 }
489 let rows: Vec<&str> = crate::cics_tables::resp_table().iter().map(|r| r.condition).collect();
490 let names: Vec<&str> = Condition::ALL.iter().map(|c| c.name()).collect();
491 assert_eq!(names, rows, "one variant per DFHRESP row, in its order");
492 }
493
494 #[test]
495 fn every_default_abend_is_for_a_dfhresp_condition_and_listed_once() {
496 for (i, &(c, abend)) in DEFAULT_ABENDS.iter().enumerate() {
497 assert!(crate::cics_tables::resp(c.name()).is_some(), "{c:?}");
498 assert!(DEFAULT_ABENDS[..i].iter().all(|&(d, _)| d != c), "{c:?} is listed twice");
499 assert_eq!(c.default_abend(), abend);
500 }
501 assert_eq!(DEFAULT_ABENDS.len(), 38);
502 let codes = [Condition::QIDERR, Condition::ROLLEDBACK, Condition::FILENOTFOUND, Condition::INVREQ, Condition::INVMPSZ].map(Condition::default_abend);
503 assert_eq!(codes, ["AEYH", "AEXJ", "AEIL", "AEIP", "AEYB"]);
504 }
505
506 #[test]
507 fn dsiderr_is_filenotfound_by_its_older_name() {
508 let c = Condition::from_name("dsiderr").unwrap();
509 assert_eq!((c, c.name(), c.resp()), (Condition::FILENOTFOUND, "FILENOTFOUND", 12));
510 assert_eq!(Condition::from_name("NOSUCH"), None);
511 }
512
513 #[test]
514 fn parse_file_ksds() {
515 let (name, def) = parse_file("MYFILE=/tmp/data,KSDS,key=0:8,len=80").unwrap();
516 assert_eq!(name, "MYFILE");
517 assert_eq!(def.record_len, 80);
518 assert!(matches!(&def.data_set, DataSet::Ksds { key } if key.offset == 0 && key.len == 8));
519 }
520
521 #[test]
522 fn parse_file_rrds_text() {
523 let (name, def) = parse_file("myfile=/tmp/data,rrds,len=120,text").unwrap();
524 assert_eq!(name, "MYFILE");
525 assert_eq!(def.record_len, 120);
526 assert!(matches!(def.data_set, DataSet::Rrds));
527 assert_eq!(def.dd.format, Some(Format::Text));
528 }
529
530 #[test]
531 fn parse_file_errors() {
532 assert!(parse_file("F=/tmp/d,KSDS,len=80").is_err());
533 assert!(parse_file("F=/tmp/d,RRDS,len=abc").is_err());
534 assert!(parse_file("F=/tmp/d,FOO,len=80").is_err());
535 }
536
537 #[test]
538 fn ts_queue() {
539 let mut task = Task::default();
540 task.writeq_ts("Q1", None, b"one").unwrap();
541 task.writeq_ts("Q1", None, b"two").unwrap();
542 task.writeq_ts("Q1", None, b"three").unwrap();
543
544 let (data, count) = task.readq_ts("Q1", Some(2)).unwrap();
545 assert_eq!(data, b"two");
546 assert_eq!(count, 3);
547
548 let (data, count) = task.readq_ts("Q1", None).unwrap();
549 assert_eq!(data, b"three");
550 assert_eq!(count, 3);
551
552 assert_eq!(task.readq_ts("Q1", None), Err(Condition::ITEMERR));
553
554 task.writeq_ts("Q1", Some(1), b"ONE").unwrap();
555 let (data, _) = task.readq_ts("Q1", Some(1)).unwrap();
556 assert_eq!(data, b"ONE");
557
558 assert_eq!(task.writeq_ts("Q1", Some(9), b"x"), Err(Condition::ITEMERR));
559 assert_eq!(task.readq_ts("NOPE", None), Err(Condition::QIDERR));
560
561 task.deleteq_ts("Q1").unwrap();
562 assert_eq!(task.readq_ts("Q1", None), Err(Condition::QIDERR));
563 }
564
565 #[test]
566 fn td_queue() {
567 let mut task = Task::default();
568 task.writeq_td("Q1", b"first");
569 task.writeq_td("Q1", b"second");
570 task.writeq_td("Q1", b"third");
571
572 assert_eq!(task.readq_td("Q1").unwrap(), b"first");
573 assert_eq!(task.readq_td("Q1").unwrap(), b"second");
574 assert_eq!(task.readq_td("Q1").unwrap(), b"third");
575 assert_eq!(task.readq_td("Q1"), Err(Condition::QZERO));
576 }
577
578 #[test]
579 fn abstime_and_unix() {
580 assert_eq!(abstime(0, 0), 2_208_988_800_000);
581 assert_eq!(abstime(1, 50), 2_208_988_801_500);
582 assert_eq!(unix_seconds(abstime(1, 50)), 1);
583 assert_eq!(unix_seconds(abstime(0, 0)), 0);
584 }
585
586 #[test]
587 fn eib_date_and_time() {
588 let at = abstime(1_790_514_309, 0);
590 assert_eq!(eib_date(at), 126_270);
591 assert_eq!(eib_time(at), 130_509);
592
593 let at = abstime(946_598_400, 0);
595 assert_eq!(eib_date(at), 99_365);
596 assert_eq!(eib_time(at), 0);
597 }
598
599 #[test]
600 fn format_time_options() {
601 let at = abstime(1_790_514_309, 25);
602
603 assert_eq!(format_time(at, "YYYYMMDD", Some('/'), None), Some(FormatValue::Text("2026/09/27".into())));
604 assert_eq!(format_time(at, "MMDDYY", None, None), Some(FormatValue::Text("092726".into())));
605 assert_eq!(format_time(at, "YYDDD", Some('-'), None), Some(FormatValue::Text("26-270".into())));
606 assert_eq!(format_time(at, "TIME", None, Some(':')), Some(FormatValue::Text("13:05:09".into())));
607 assert_eq!(format_time(at, "DAYOFWEEK", None, None), Some(FormatValue::Number(0)));
608 assert_eq!(format_time(at, "DAYOFMONTH", None, None), Some(FormatValue::Number(27)));
609 assert_eq!(format_time(at, "MONTHOFYEAR", None, None), Some(FormatValue::Number(9)));
610 assert_eq!(format_time(at, "YEAR", None, None), Some(FormatValue::Number(2026)));
611 assert_eq!(format_time(at, "MILLISECONDS", None, None), Some(FormatValue::Number(250)));
612 assert_eq!(format_time(at, "DAYCOUNT", None, None), Some(FormatValue::Number(46_290)));
613 assert_eq!(format_time(at, "STRINGFORMAT", None, None), None);
614 }
615}