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