1use crate::calendar::{civil, days_in_month, is_leap, lilian, weekday, LAST_LILIAN, LILIAN_ZERO, MILLIS_PER_DAY, SECONDS_PER_DAY};
7use numeric::precision::{Fixed, Places};
8use zarch::check::ProgramMask;
9use zarch::ebcdic::{self, CodePage};
10use zarch::hfp::{Hfp, Precision, Rounding};
11use zarch::wide::U256;
12
13pub const PROVIDED: &[(&str, &[&str])] = &[
15 ("CEE3ABD", &["abcode", "clean-up"]),
16 ("CEE3DMP", &["title", "options", "fc"]),
17 ("CEEDATE", &["input_Lilian_date", "picture_string", "output_char_date", "fc"]),
18 ("CEEDATM", &["input_seconds", "picture_string", "output_timestamp", "fc"]),
19 ("CEEDAYS", &["input_char_date", "picture_string", "output_Lilian_date", "fc"]),
20 ("CEEDYWK", &["input_Lilian_date", "output_day_no", "fc"]),
21 ("CEEFRST", &["address", "fc"]),
22 ("CEEGMT", &["output_GMT_Lilian", "output_GMT_seconds", "fc"]),
23 ("CEEGMTO", &["offset_hours", "offset_minutes", "offset_seconds", "fc"]),
24 ("CEEGTST", &["heap_id", "size", "address", "fc"]),
25 ("CEELOCT", &["output_Lilian", "output_seconds", "output_Gregorian", "fc"]),
26 ("CEEMOUT", &["message_string", "destination_code", "fc"]),
27 ("CEESECS", &["input_timestamp", "picture_string", "output_seconds", "fc"]),
28 ("CEEUTC", &["output_GMT_Lilian", "output_GMT_seconds", "fc"]),
29];
30
31const DOCUMENTED: &[&str] = &[
33 "CEE3AB2", "CEE3ABD", "CEE3CIB", "CEE3CTY", "CEE3DLY", "CEE3DMP", "CEE3GRC", "CEE3GRN", "CEE3GRO", "CEE3INF", "CEE3LNG", "CEE3MC2", "CEE3MCS",
34 "CEE3MDS", "CEE3MTS", "CEE3PR2", "CEE3PRM", "CEE3RPH", "CEE3SPM", "CEE3SRC", "CEE3SRP", "CEE3USR", "CEECBLDY", "CEECMI", "CEECRHP", "CEECZST",
35 "CEEDATE", "CEEDATM", "CEEDAYS", "CEEDCOD", "CEEDLYM", "CEEDSHP", "CEEDYWK", "CEEENV", "CEEFMDA", "CEEFMDT", "CEEFMON", "CEEFMTM", "CEEFRST",
36 "CEEFTDS", "CEEGMT", "CEEGMTO", "CEEGPID", "CEEGQDT", "CEEGTJS", "CEEGTST", "CEEHDLR", "CEEHDLU", "CEEISEC", "CEEITOK", "CEELCNV", "CEELOCT",
37 "CEEMGET", "CEEMICT", "CEEMOUT", "CEEMRCE", "CEEMRCR", "CEEMSG", "CEENCOD", "CEEQCEN", "CEEQDTC", "CEEQRYL", "CEERAN0", "CEERCDM", "CEESCEN",
38 "CEESCOL", "CEESECI", "CEESECS", "CEESETL", "CEESGL", "CEESICLR", "CEESISET", "CEESISHF", "CEESITST", "CEESTXF", "CEETDLI", "CEETEST", "CEEUSGD",
39 "CEEUTC",
40];
41
42const MATH: &[&str] = &[
44 "ABS", "ACS", "ASN", "ATH", "ATN", "AT2", "CJG", "COS", "CSH", "CTN", "DIM", "DVD", "ERC", "ERF", "EXP", "GMA", "IMG", "INT", "LGM", "LG1", "LG2",
45 "LOG", "MLT", "MOD", "NIN", "NWN", "SGN", "SIN", "SNH", "SQT", "TAN", "TNH",
46];
47
48pub fn parameters(name: &str) -> Option<&'static [&'static str]> {
49 PROVIDED.iter().find(|(n, _)| *n == name).map(|(_, p)| *p)
50}
51
52pub fn provides(name: &str) -> bool {
53 parameters(name).is_some()
54}
55
56fn documented(name: &str) -> bool {
57 let math = name.len() == 8 && name.starts_with("CEES") && (MATH.contains(&&name[5..]) || name[5..].starts_with("XP"));
58 DOCUMENTED.contains(&name) || math
59}
60
61pub fn missing(name: &str) -> String {
63 if documented(name) {
64 format!("CALL {name}: {name} is a Language Environment callable service that ironwork for COBOL does not provide yet")
65 } else {
66 format!("CALL {name}: no such program in the run unit or its program libraries")
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub struct Condition {
73 pub severity: u8,
74 pub number: u16,
75}
76
77const fn condition(severity: u8, number: u16) -> Condition {
78 Condition { severity, number }
79}
80
81pub const DESTINATION: Condition = condition(3, 451);
82pub const HEAP_ID: Condition = condition(3, 803);
83pub const HEAP_SIZE: Condition = condition(3, 808);
84pub const FREE_ADDRESS: Condition = condition(3, 810);
85pub const HEAP_SHORT: Condition = condition(3, 813);
86pub const SECONDS_RANGE: Condition = condition(3, 2505);
87pub const INSUFFICIENT: Condition = condition(3, 2507);
88pub const DATE_VALUE: Condition = condition(3, 2508);
89pub const HOURS: Condition = condition(3, 2510);
90pub const LILIAN_RANGE: Condition = condition(3, 2512);
91pub const DATE_RANGE: Condition = condition(3, 2513);
92pub const MINUTES: Condition = condition(3, 2516);
93pub const MONTH: Condition = condition(3, 2517);
94pub const PICTURE: Condition = condition(3, 2518);
95pub const SECONDS_VALUE: Condition = condition(3, 2519);
96pub const DAYS_NONNUMERIC: Condition = condition(3, 2520);
97pub const SECS_NONNUMERIC: Condition = condition(3, 2525);
98pub const DATE_TRUNCATED: Condition = condition(2, 2526);
99pub const TIMESTAMP_TRUNCATED: Condition = condition(2, 2527);
100pub const DUMP_OPTIONS: Condition = condition(2, 3102);
101
102impl Condition {
103 pub fn symbol(self) -> String {
104 syntax::feedback::symbol(self.number)
105 }
106
107 pub fn token(self) -> [u8; 12] {
109 let mut t = [0u8; 12];
110 t[..8].copy_from_slice(&syntax::feedback::token(self.severity, self.number));
111 t
112 }
113
114 pub fn text(self) -> &'static str {
115 match self.number {
116 451 => "CEEMOUT was given a destination code it does not accept.",
117 803 => "No heap has the identifier given to a get-storage or discard-heap request.",
118 808 => "A get-storage (CEEGTST) or reallocate (CEECZST) request asked for a size that is not positive.",
119 810 => "CEEFRST was given an address that is not heap storage, or the heap's control information is damaged.",
120 813 => "There was not enough storage to satisfy a get-storage request.",
121 2505 => "The number of seconds is outside the range the service supports.",
122 2507 => "CEEDAYS or CEESECS ran out of input before the date was complete, so no Lilian value was produced.",
123 2508 => "CEEDAYS or CEESECS was given a date that does not exist.",
124 2510 => "CEEISEC or CEESECS was given an hour it cannot accept.",
125 2512 => "CEEDATE or CEEDYWK was given a Lilian day outside the supported range.",
126 2513 => "CEEISEC, CEEDAYS or CEESECS was given a date outside the supported range.",
127 2516 => "CEEISEC was given a minute it cannot accept.",
128 2517 => "CEEISEC was given a month it cannot accept.",
129 2518 => "A date or time service was given a picture string it cannot use.",
130 2519 => "CEEISEC was given a second it cannot accept.",
131 2520 => "CEEDAYS found a non-digit where its picture wants a number, or the date does not fit the picture.",
132 2525 => "CEESECS found a non-digit where its picture wants a number, or the timestamp does not fit the picture.",
133 2526 => "The date CEEDATE produced did not fit its output field and was cut short.",
134 2527 => "The timestamp CEEDATM produced did not fit its output field and was cut short.",
135 3102 => "CEE3DMP ignored options or suboptions it does not recognise.",
136 _ => "",
137 }
138 }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub struct Stamp {
144 pub lilian: i64,
145 pub millis: i64,
146}
147
148impl Stamp {
149 pub fn from_unix(seconds: i64, hundredths: u32) -> Self {
151 Self { lilian: seconds.div_euclid(SECONDS_PER_DAY) - LILIAN_ZERO, millis: seconds.rem_euclid(SECONDS_PER_DAY) * 1000 + i64::from(hundredths.min(99)) * 10 }
152 }
153
154 pub fn from_millis(ms: i64) -> Self {
155 Self { lilian: ms.div_euclid(MILLIS_PER_DAY), millis: ms.rem_euclid(MILLIS_PER_DAY) }
156 }
157
158 pub fn total_millis(self) -> i64 {
159 self.lilian.saturating_mul(MILLIS_PER_DAY).saturating_add(self.millis)
160 }
161
162 pub fn fields(self) -> Fields {
163 let c = civil(self.lilian.saturating_add(LILIAN_ZERO).saturating_mul(SECONDS_PER_DAY));
164 let s = self.millis / 1000;
165 Fields {
166 year: c.year,
167 month: c.month,
168 day: c.day,
169 yday: c.day_of_year,
170 weekday: weekday(self.lilian),
171 hour: (s / 3600) as u32,
172 minute: (s / 60 % 60) as u32,
173 second: (s % 60) as u32,
174 millis: (self.millis % 1000) as u32,
175 }
176 }
177
178 pub fn gregorian(self) -> String {
180 let f = self.fields();
181 format!("{:04}{:02}{:02}{:02}{:02}{:02}{:03}", f.year, f.month, f.day, f.hour, f.minute, f.second, f.millis)
182 }
183}
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
186pub struct Fields {
187 pub year: i64,
188 pub month: u32,
189 pub day: u32,
190 pub yday: u32,
191 pub weekday: u32,
192 pub hour: u32,
193 pub minute: u32,
194 pub second: u32,
195 pub millis: u32,
196}
197
198pub fn seconds_hfp(total_millis: i64) -> [u8; 8] {
200 let value = Fixed { negative: total_millis < 0, magnitude: U256::from_u128(u128::from(total_millis.unsigned_abs())), places: Places::new(16, 3) };
201 let bytes = numeric::float::from_fixed(value, Precision::Long, ProgramMask::default()).map_or(vec![0; 8], Hfp::to_bytes);
202 bytes.try_into().unwrap_or([0; 8])
203}
204
205pub fn hfp_millis(bytes: [u8; 8]) -> Option<i64> {
208 let (negative, ms) = Hfp::from_bytes(Precision::Long, &bytes).to_scaled_integer(3, Rounding::HalfAwayFromZero)?;
209 let ms = i64::try_from(ms.to_u128()?).ok()?;
210 Some(if negative { -ms } else { ms })
211}
212
213const MONTHS: [&str; 12] = ["JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER"];
214const DAYS: [&str; 7] = ["SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY"];
215const ROMAN: [&str; 12] = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
216
217#[derive(Clone, Debug, PartialEq, Eq)]
219pub enum Term {
220 Year(u8),
222 Month(bool),
224 MonthName { upper: Vec<bool>, trim: bool },
226 Roman(bool),
228 Day(bool),
229 DayOfYear,
230 Hour(bool),
231 Minute,
232 Second,
233 Fraction(u8),
235 Meridiem { lower: bool, dots: bool },
236 Weekday,
237 WeekdayName { upper: Vec<bool>, trim: bool },
238 Era,
240 Literal(u8),
242}
243
244impl Term {
245 fn is_time(&self) -> bool {
246 matches!(self, Term::Hour(_) | Term::Minute | Term::Second | Term::Fraction(_) | Term::Meridiem { .. })
247 }
248}
249
250const DEFAULT_DATE: &str = "MM/DD/YY";
252const DEFAULT_TIMESTAMP: &str = "MM/DD/YY ZH:MI:SS AP";
253
254fn blank(picture: &[u8]) -> bool {
255 picture.iter().all(|&b| b == ebcdic::SPACE)
256}
257
258fn picture_or_default(picture: &[u8], default: &str, page: &CodePage) -> Vec<u8> {
259 if blank(picture) { page.encode(default).unwrap_or_default() } else { picture.to_vec() }
260}
261
262pub fn terms(picture: &[u8], page: &CodePage) -> Vec<Term> {
263 let chars: Vec<char> = picture.iter().map(|&b| page.decode_byte(b)).collect();
264 let at = |i: usize| chars.get(i).copied().unwrap_or('\0');
265 let run = |i: usize, f: fn(char) -> bool| chars[i..].iter().take_while(|&&c| f(c)).count();
266 let starts = |i: usize, s: &str| chars[i..].iter().copied().take(s.chars().count()).eq(s.chars());
267 let trim = |i: usize| matches!(at(i), 'Z' | 'z') && !(at(i) == 'Z' && (matches!(at(i + 1), 'M' | 'D' | 'H') || starts(i + 1, "YY")));
269 let mut out: Vec<Term> = Vec::new();
270 let mut i = 0;
271 while i < chars.len() {
272 let after_hour = matches!(out.iter().rev().find(|t| !matches!(t, Term::Literal(_))), Some(Term::Hour(_)));
273 let (term, used) = match chars[i] {
274 'Y' => match run(i, |c| c == 'Y').min(4) {
275 3 => (Term::Era, 3),
276 n => (Term::Year(n as u8), n),
277 },
278 'Z' => match at(i + 1) {
279 'M' => (Term::Month(true), 2),
280 'D' => (Term::Day(true), 2),
281 'H' => (Term::Hour(true), 2),
282 'Y' if at(i + 2) == 'Y' => (Term::Era, 3),
283 _ => (Term::Literal(picture[i]), 1),
284 },
285 'M' if at(i + 1) == 'I' => (Term::Minute, 2),
286 'M' => match run(i, |c| c == 'M' || c == 'm') {
287 1 => (Term::Literal(picture[i]), 1),
288 2 if at(i + 1) == 'M' => (if after_hour { Term::Minute } else { Term::Month(false) }, 2),
289 2 => (Term::Literal(picture[i]), 1),
290 n => {
291 let width = n.min(20);
292 let upper = chars[i..i + width].iter().map(|c| c.is_ascii_uppercase()).collect();
293 let trim = width == n && trim(i + width);
294 (Term::MonthName { upper, trim }, width + usize::from(trim))
295 }
296 },
297 'W' => match run(i, |c| c == 'W' || c == 'w') {
298 1 | 2 => (Term::Weekday, 1),
299 n => {
300 let width = n.min(20);
301 let upper = chars[i..i + width].iter().map(|c| c.is_ascii_uppercase()).collect();
302 let trim = width == n && trim(i + width);
303 (Term::WeekdayName { upper, trim }, width + usize::from(trim))
304 }
305 },
306 'R' => match run(i, |c| c == 'R') {
307 n if n >= 4 => (Term::Roman(false), 4),
308 3 if at(i + 3) == 'Z' => (Term::Roman(true), 4),
309 _ => (Term::Literal(picture[i]), 1),
310 },
311 'D' => match run(i, |c| c == 'D') {
312 1 => (Term::Literal(picture[i]), 1),
313 2 => (Term::Day(false), 2),
314 _ => (Term::DayOfYear, 3),
315 },
316 'H' if at(i + 1) == 'H' => (Term::Hour(false), 2),
317 'S' if at(i + 1) == 'S' => (Term::Second, 2),
318 '9' => {
319 let n = run(i, |c| c == '9').min(3);
320 (Term::Fraction(n as u8), n)
321 }
322 'A' | 'a' if starts(i + 1, if chars[i] == 'A' { "P" } else { "p" }) => (Term::Meridiem { lower: chars[i] == 'a', dots: false }, 2),
323 'A' | 'a' if starts(i, if chars[i] == 'A' { "A.P." } else { "a.p." }) => (Term::Meridiem { lower: chars[i] == 'a', dots: true }, 4),
324 '<' if starts(i, "<JJJJ>") || starts(i, "<CCCC>") => (Term::Era, 6),
325 '<' if starts(i, "<CCCCCCCC>") => (Term::Era, 10),
326 _ => (Term::Literal(picture[i]), 1),
327 };
328 out.push(term);
329 i += used;
330 }
331 out
332}
333
334fn cased(name: &str, upper: &[bool], trim: bool) -> String {
336 let mut s: String = name.chars().zip(upper).map(|(c, &u)| if u { c } else { c.to_ascii_lowercase() }).collect();
337 if !trim {
338 while s.len() < upper.len() {
339 s.push(' ');
340 }
341 }
342 s
343}
344
345pub fn format(terms: &[Term], f: &Fields, time: bool, page: &CodePage) -> Vec<u8> {
348 let twelve = time && terms.iter().any(|t| matches!(t, Term::Meridiem { .. }));
349 let (hour, minute, second, millis) = if time { (f.hour, f.minute, f.second, f.millis) } else { (0, 0, 0, 0) };
350 let clock_hour = if twelve && hour % 12 == 0 { 12 } else if twelve { hour % 12 } else { hour };
351 let mut out = Vec::new();
352 for t in terms {
353 let text = match t {
354 Term::Literal(b) => {
355 out.push(*b);
356 continue;
357 }
358 Term::Year(4) => format!("{:04}", f.year),
359 Term::Year(2) => format!("{:02}", f.year % 100),
360 Term::Year(_) => format!("{}", f.year % 10),
361 Term::Month(true) => f.month.to_string(),
362 Term::Month(false) => format!("{:02}", f.month),
363 Term::MonthName { upper, trim } => cased(MONTHS[f.month as usize - 1], upper, *trim),
364 Term::Roman(trim) => cased(ROMAN[f.month as usize - 1], &[true; 4], *trim),
365 Term::Day(true) => f.day.to_string(),
366 Term::Day(false) => format!("{:02}", f.day),
367 Term::DayOfYear => format!("{:03}", f.yday),
368 Term::Hour(true) => clock_hour.to_string(),
369 Term::Hour(false) => format!("{clock_hour:02}"),
370 Term::Minute => format!("{minute:02}"),
371 Term::Second => format!("{second:02}"),
372 Term::Fraction(n) => format!("{:03}", millis)[..*n as usize].to_owned(),
373 Term::Meridiem { lower, dots } => {
374 let pm = time && hour >= 12;
375 let s = match (pm, dots) {
376 (false, false) => "AM",
377 (true, false) => "PM",
378 (false, true) => "A.M.",
379 (true, true) => "P.M.",
380 };
381 if *lower { s.to_ascii_lowercase() } else { s.to_owned() }
382 }
383 Term::Weekday => DAYS[f.weekday as usize - 1][..1].to_owned(),
384 Term::WeekdayName { upper, trim } => cased(DAYS[f.weekday as usize - 1], upper, *trim),
385 Term::Era => String::new(),
386 };
387 out.extend(text.chars().map(|c| page.encode_char(c).unwrap_or(ebcdic::SPACE)));
388 }
389 out
390}
391
392fn output_80(mut text: Vec<u8>) -> (Vec<u8>, bool) {
394 let truncated = text.len() > 80;
395 text.resize(80, ebcdic::SPACE);
396 (text, truncated)
397}
398
399pub fn date(lilian: i64, picture: &[u8], page: &CodePage) -> (Vec<u8>, Option<Condition>) {
401 let blanks = vec![ebcdic::SPACE; 80];
402 if !(1..=LAST_LILIAN).contains(&lilian) {
403 return (blanks, Some(LILIAN_RANGE));
404 }
405 let terms = terms(&picture_or_default(picture, DEFAULT_DATE, page), page);
406 if terms.contains(&Term::Era) {
407 return (blanks, Some(PICTURE));
408 }
409 let fields = Stamp { lilian, millis: 0 }.fields();
410 let (text, truncated) = output_80(format(&terms, &fields, false, page));
411 (text, truncated.then_some(DATE_TRUNCATED))
412}
413
414pub fn timestamp(seconds: [u8; 8], picture: &[u8], page: &CodePage) -> (Vec<u8>, Option<Condition>) {
416 let blanks = vec![ebcdic::SPACE; 80];
417 let Some(ms) = hfp_millis(seconds).filter(|ms| (MILLIS_PER_DAY..(LAST_LILIAN + 1) * MILLIS_PER_DAY).contains(ms)) else {
418 return (blanks, Some(SECONDS_RANGE));
419 };
420 let terms = terms(&picture_or_default(picture, DEFAULT_TIMESTAMP, page), page);
421 if terms.contains(&Term::Era) {
422 return (blanks, Some(PICTURE));
423 }
424 let (text, truncated) = output_80(format(&terms, &Stamp::from_millis(ms).fields(), true, page));
425 (text, truncated.then_some(TIMESTAMP_TRUNCATED))
426}
427
428#[derive(Clone, Copy, Debug, PartialEq, Eq)]
430pub enum Reading {
431 Days,
432 Seconds,
433}
434
435fn number(input: &[char], at: &mut usize, width: usize, delimited: bool, nonnumeric: Condition) -> Result<u32, Condition> {
439 if width == 0 {
440 *at += 1;
441 return Ok(0);
442 }
443 let (mut value, mut digits, mut taken) = (0u32, 0, 0);
444 while taken < width && *at + taken < input.len() {
445 match input[*at + taken] {
446 ' ' if digits == 0 => {}
447 c if c.is_ascii_digit() => {
448 value = value * 10 + c.to_digit(10).unwrap_or(0);
449 digits += 1;
450 }
451 _ => break,
452 }
453 taken += 1;
454 }
455 *at += taken;
456 let complete = digits > 0 && (taken == width || delimited);
457 match (complete, *at < input.len()) {
458 (true, _) => Ok(value),
459 (false, true) => Err(nonnumeric),
460 (false, false) => Err(INSUFFICIENT),
461 }
462}
463
464fn word(input: &[char], at: &mut usize, f: fn(&char) -> bool) -> String {
466 let w: String = input[*at..].iter().take_while(|c| f(c)).take(20).collect();
467 *at += w.chars().count();
468 w.to_ascii_uppercase()
469}
470
471#[derive(Default)]
472struct Parsed {
473 year: Option<(i64, bool)>,
474 month: Option<u32>,
475 day: Option<u32>,
476 yday: Option<u32>,
477 hour: Option<u32>,
478 minute: u32,
479 second: u32,
480 millis: u32,
481 pm: Option<bool>,
482}
483
484pub fn read(input: &[u8], picture: &[u8], reading: Reading, window: i64, page: &CodePage) -> Result<Stamp, Condition> {
487 let default = if reading == Reading::Days { DEFAULT_DATE } else { DEFAULT_TIMESTAMP };
488 let picture = picture_or_default(picture, default, page);
489 let terms = terms(&picture, page);
490 if terms.iter().any(|t| matches!(t, Term::Era | Term::Year(1))) {
491 return Err(PICTURE);
492 }
493 let nonnumeric = if reading == Reading::Days { DAYS_NONNUMERIC } else { SECS_NONNUMERIC };
494 let input: Vec<char> = input.iter().map(|&b| page.decode_byte(b)).collect();
495 let lead = terms.iter().take_while(|t| **t == Term::Literal(ebcdic::SPACE)).count();
496 let mut at = if lead > 0 { lead } else { input.iter().take_while(|&&c| c == ' ').count() };
497 let mut p = Parsed::default();
498 for (k, t) in terms.iter().enumerate().skip(lead) {
499 if at >= input.len() {
500 if terms[k..].iter().any(|t| !t.is_time() && !matches!(t, Term::Literal(_) | Term::Weekday | Term::WeekdayName { .. })) {
501 return Err(INSUFFICIENT);
502 }
503 break;
504 }
505 let delimited = matches!(terms.get(k + 1), Some(Term::Literal(_)));
506 let mut number = |width: u8| number(&input, &mut at, width as usize, delimited, nonnumeric);
507 let outcome = match t {
508 Term::Literal(_) => number(0).map(drop),
509 Term::Year(n) => number(*n).map(|y| p.year = Some((i64::from(y), *n == 2))),
510 Term::Month(_) => number(2).map(|m| p.month = Some(m)),
511 Term::Day(_) => number(2).map(|d| p.day = Some(d)),
512 Term::DayOfYear => number(3).map(|d| p.yday = Some(d)),
513 Term::Hour(_) => number(2).map(|h| p.hour = Some(h)),
514 Term::Minute => number(2).map(|m| p.minute = m),
515 Term::Second => number(2).map(|s| p.second = s),
516 Term::Fraction(n) => number(*n).map(|f| p.millis = f * 10u32.pow(3 - u32::from(*n))),
517 Term::MonthName { .. } => {
518 let word = word(&input, &mut at, |c| c.is_ascii_alphabetic());
519 MONTHS.iter().position(|m| word.len() >= 3 && m.starts_with(&word)).map(|m| p.month = Some(m as u32 + 1)).ok_or(MONTH)
520 }
521 Term::Roman(_) => {
522 let word = word(&input, &mut at, |c| matches!(c, 'I' | 'V' | 'X' | 'i' | 'v' | 'x'));
523 ROMAN.iter().position(|r| *r == word).map(|m| p.month = Some(m as u32 + 1)).ok_or(MONTH)
524 }
525 Term::Weekday | Term::WeekdayName { .. } => {
526 word(&input, &mut at, |c| c.is_ascii_alphabetic());
527 Ok(())
528 }
529 Term::Meridiem { dots, .. } => {
530 let word: String = input[at..].iter().take(if *dots { 4 } else { 2 }).collect::<String>().to_ascii_uppercase();
531 at += word.chars().count();
532 match word.as_str() {
533 "AM" | "A.M." => Some(false),
534 "PM" | "P.M." => Some(true),
535 _ => None,
536 }
537 .map(|pm| p.pm = Some(pm))
538 .ok_or(nonnumeric)
539 }
540 Term::Era => Err(PICTURE),
541 };
542 if !(reading == Reading::Days && t.is_time()) {
543 outcome?;
544 }
545 }
546 let Some((year, two_digit)) = p.year else { return Err(INSUFFICIENT) };
547 let year = if two_digit { window + (year - window).rem_euclid(100) } else { year };
548 let lilian = match (p.month, p.day, p.yday) {
549 (_, _, Some(yday)) if p.month.is_none() || p.day.is_none() => {
550 let length = if is_leap(year) { 366 } else { 365 };
551 if !(1..=length).contains(&yday) {
552 return Err(DATE_VALUE);
553 }
554 lilian(year, 1, 1) + i64::from(yday) - 1
555 }
556 (Some(month), Some(day), _) => {
557 if !(1..=12).contains(&month) {
558 return Err(MONTH);
559 }
560 if !(1..=days_in_month(year, month)).contains(&day) {
561 return Err(DATE_VALUE);
562 }
563 lilian(year, month, day)
564 }
565 _ => return Err(INSUFFICIENT),
566 };
567 if !(1582..=9999).contains(&year) || !(1..=LAST_LILIAN).contains(&lilian) {
568 return Err(DATE_RANGE);
569 }
570 if reading == Reading::Days {
571 return Ok(Stamp { lilian, millis: 0 });
572 }
573 let hour = match (p.hour.unwrap_or(0), p.pm) {
574 (h, None) if h <= 23 => h,
575 (h @ 1..=12, Some(pm)) => h % 12 + if pm { 12 } else { 0 },
576 _ => return Err(HOURS),
577 };
578 if p.minute > 59 {
579 return Err(MINUTES);
580 }
581 if p.second > 59 {
582 return Err(SECONDS_VALUE);
583 }
584 let millis = ((i64::from(hour) * 60 + i64::from(p.minute)) * 60 + i64::from(p.second)) * 1000 + i64::from(p.millis);
585 Ok(Stamp { lilian, millis })
586}
587
588#[derive(Debug, Default)]
590pub struct State {
591 pub heap: Vec<(usize, usize, bool)>,
593 pub written: Vec<String>,
594}
595
596impl State {
597 pub fn heap_end(&self) -> usize {
599 self.heap.iter().map(|&(at, len, _)| at + len).max().unwrap_or(0)
600 }
601}
602
603pub fn dump_options(options: &str) -> (String, bool) {
605 const KEYWORDS: &[(&str, usize)] = &[
606 ("ENCLAVE", 4), ("THREAD", 3), ("TRACEBACK", 5), ("NOTRACEBACK", 7), ("FILES", 4), ("NOFILES", 6), ("VARIABLES", 3),
607 ("NOVARIABLES", 5), ("BLOCKS", 5), ("NOBLOCKS", 7), ("STORAGE", 4), ("NOSTORAGE", 6), ("REGSTOR", 4), ("STACKFRAME", 2),
608 ("PAGESIZE", 4), ("FNAME", 5), ("CONDITION", 4), ("NOCONDITION", 6), ("ENTRY", 5), ("NOENTRY", 7), ("GENOPTS", 4), ("NOGENOPTS", 6),
609 ];
610 let mut dd = "CEEDUMP".to_owned();
611 let mut invalid = false;
612 let upper = options.to_ascii_uppercase();
613 let mut rest = upper.as_str();
614 loop {
615 rest = rest.trim_start_matches([' ', ',']);
616 if rest.is_empty() {
617 break;
618 }
619 let word_end = rest.find([' ', ',', '(']).unwrap_or(rest.len());
620 let (word, after) = rest.split_at(word_end);
621 let (argument, after) = match after.strip_prefix('(') {
622 Some(inner) => match inner.split_once(')') {
623 Some((a, tail)) => (Some(a.trim()), tail),
624 None => {
625 invalid = true;
626 (Some(inner.trim()), "")
627 }
628 },
629 None => (None, after),
630 };
631 match KEYWORDS.iter().find(|(k, min)| word.len() >= *min && k.starts_with(word)) {
632 Some(("FNAME", _)) => match argument.filter(|a| !a.is_empty()) {
633 Some(a) => dd = a.to_owned(),
634 None => invalid = true,
635 },
636 Some(_) => {}
637 None => invalid = true,
638 }
639 rest = after;
640 }
641 (dd, invalid)
642}
643
644#[cfg(test)]
645mod tests;