1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2use copybook_codepage::Codepage;
25use copybook_error::{Error, ErrorCode, Result};
26use std::convert::TryFrom;
27
28#[cfg(test)]
29const DEFAULT_PROPTEST_CASE_COUNT: u32 = 512;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ZeroSignPolicy {
47 Positive,
49 Preferred,
51}
52
53static ASCII_POSITIVE_OVERPUNCH: [u8; 10] = [
58 b'{', b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', ];
69
70static ASCII_NEGATIVE_OVERPUNCH: [u8; 10] = [
73 b'}', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', ];
84
85static ASCII_OVERPUNCH_DECODE: [Option<(u8, bool)>; 256] = {
88 let mut table = [None; 256];
89
90 table[b'{' as usize] = Some((0, false));
92 table[b'A' as usize] = Some((1, false));
93 table[b'B' as usize] = Some((2, false));
94 table[b'C' as usize] = Some((3, false));
95 table[b'D' as usize] = Some((4, false));
96 table[b'E' as usize] = Some((5, false));
97 table[b'F' as usize] = Some((6, false));
98 table[b'G' as usize] = Some((7, false));
99 table[b'H' as usize] = Some((8, false));
100 table[b'I' as usize] = Some((9, false));
101
102 table[b'}' as usize] = Some((0, true));
104 table[b'J' as usize] = Some((1, true));
105 table[b'K' as usize] = Some((2, true));
106 table[b'L' as usize] = Some((3, true));
107 table[b'M' as usize] = Some((4, true));
108 table[b'N' as usize] = Some((5, true));
109 table[b'O' as usize] = Some((6, true));
110 table[b'P' as usize] = Some((7, true));
111 table[b'Q' as usize] = Some((8, true));
112 table[b'R' as usize] = Some((9, true));
113
114 table[b'0' as usize] = Some((0, false));
116 table[b'1' as usize] = Some((1, false));
117 table[b'2' as usize] = Some((2, false));
118 table[b'3' as usize] = Some((3, false));
119 table[b'4' as usize] = Some((4, false));
120 table[b'5' as usize] = Some((5, false));
121 table[b'6' as usize] = Some((6, false));
122 table[b'7' as usize] = Some((7, false));
123 table[b'8' as usize] = Some((8, false));
124 table[b'9' as usize] = Some((9, false));
125
126 table
127};
128
129#[must_use]
133#[inline]
134pub fn encode_ebcdic_overpunch_zone(digit: u8, is_negative: bool, policy: ZeroSignPolicy) -> u8 {
135 debug_assert!(digit <= 9, "Digit must be 0-9");
136
137 if digit == 0 && policy == ZeroSignPolicy::Preferred {
138 return 0xF; }
140
141 if is_negative {
142 0xD } else {
144 0xC }
146}
147
148#[must_use]
152#[inline]
153pub const fn decode_ebcdic_overpunch_zone(zone: u8) -> Option<(bool, bool)> {
154 match zone {
155 0xC | 0xF => Some((true, false)), 0xD => Some((true, true)), _ => None, }
159}
160
161#[inline]
166#[must_use = "Handle the Result or propagate the error"]
167pub fn encode_overpunch_byte(
168 digit: u8,
169 is_negative: bool,
170 codepage: Codepage,
171 policy: ZeroSignPolicy,
172) -> Result<u8> {
173 if digit > 9 {
174 return Err(Error::new(
175 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
176 format!("Invalid digit {digit} for overpunch encoding"),
177 ));
178 }
179
180 if codepage == Codepage::ASCII {
181 if digit <= 9 {
183 let byte_value = if is_negative {
184 ASCII_NEGATIVE_OVERPUNCH[digit as usize]
185 } else {
186 ASCII_POSITIVE_OVERPUNCH[digit as usize]
187 };
188 Ok(byte_value)
189 } else {
190 Err(Error::new(
191 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
192 format!("Invalid digit {digit} for ASCII overpunch encoding"),
193 ))
194 }
195 } else {
196 let zone = encode_ebcdic_overpunch_zone(digit, is_negative, policy);
198 Ok((zone << 4) | digit)
199 }
200}
201
202#[inline]
207#[must_use = "Handle the Result or propagate the error"]
208pub fn decode_overpunch_byte(byte: u8, codepage: Codepage) -> Result<(u8, bool)> {
209 if codepage == Codepage::ASCII {
210 if let Some((digit, is_negative)) = ASCII_OVERPUNCH_DECODE[byte as usize] {
211 Ok((digit, is_negative))
212 } else {
213 Err(Error::new(
214 ErrorCode::CBKD411_ZONED_BAD_SIGN,
215 format!("Invalid ASCII overpunch byte 0x{byte:02X}"),
216 ))
217 }
218 } else {
219 let zone = (byte >> 4) & 0x0F;
221 let digit = byte & 0x0F;
222
223 if digit > 9 {
224 return Err(Error::new(
225 ErrorCode::CBKD411_ZONED_BAD_SIGN,
226 format!("Invalid digit nibble 0x{digit:X} in EBCDIC overpunch byte 0x{byte:02X}"),
227 ));
228 }
229
230 if let Some((is_signed, is_negative)) = decode_ebcdic_overpunch_zone(zone) {
231 if !is_signed {
232 return Err(Error::new(
233 ErrorCode::CBKD411_ZONED_BAD_SIGN,
234 format!("Unsigned zone 0x{zone:X} in signed EBCDIC field"),
235 ));
236 }
237 Ok((digit, is_negative))
238 } else {
239 Err(Error::new(
240 ErrorCode::CBKD411_ZONED_BAD_SIGN,
241 format!("Invalid EBCDIC zone nibble 0x{zone:X} in byte 0x{byte:02X}"),
242 ))
243 }
244 }
245}
246
247#[must_use]
249#[inline]
250pub fn is_valid_overpunch(byte: u8, codepage: Codepage) -> bool {
251 if codepage == Codepage::ASCII {
252 ASCII_OVERPUNCH_DECODE[byte as usize].is_some()
253 } else {
254 let zone = (byte >> 4) & 0x0F;
255 let digit = byte & 0x0F;
256 digit <= 9 && decode_ebcdic_overpunch_zone(zone).is_some()
257 }
258}
259
260#[must_use]
262#[inline]
263pub fn get_all_valid_overpunch_bytes(codepage: Codepage) -> Vec<u8> {
264 if codepage == Codepage::ASCII {
265 ASCII_OVERPUNCH_DECODE
266 .iter()
267 .enumerate()
268 .filter_map(|(byte, mapping)| mapping.and_then(|_| u8::try_from(byte).ok()))
269 .collect()
270 } else {
271 let mut bytes = Vec::new();
272 for zone in [0xC, 0xD, 0xF] {
273 for digit in 0..=9 {
274 bytes.push((zone << 4) | digit);
275 }
276 }
277 bytes
278 }
279}
280
281#[cfg(test)]
282#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
283mod tests {
284 use super::*;
285 use proptest::prelude::*;
286 use proptest::test_runner::RngSeed;
287 use std::collections::hash_map::DefaultHasher;
288 use std::hash::{Hash, Hasher};
289
290 fn proptest_case_count() -> u32 {
291 option_env!("PROPTEST_CASES")
292 .and_then(|s| s.parse().ok())
293 .unwrap_or(DEFAULT_PROPTEST_CASE_COUNT)
294 }
295
296 fn zoned_overpunch_proptest_config() -> ProptestConfig {
297 let mut cfg = ProptestConfig {
298 cases: proptest_case_count(),
299 max_shrink_time: 0,
300 ..ProptestConfig::default()
301 };
302
303 if let Ok(seed_value) = std::env::var("PROPTEST_SEED")
304 && !seed_value.is_empty()
305 {
306 let parsed_seed = seed_value.parse::<u64>().unwrap_or_else(|_| {
307 let mut hasher = DefaultHasher::new();
308 seed_value.hash(&mut hasher);
309 hasher.finish()
310 });
311 cfg.rng_seed = RngSeed::Fixed(parsed_seed);
312 }
313
314 cfg
315 }
316
317 proptest! {
318 #![proptest_config(zoned_overpunch_proptest_config())]
319 #[test]
320 fn prop_encode_decode_parity(
321 digit in 0u8..=9,
322 is_negative in any::<bool>(),
323 codepage in prop_oneof![
324 Just(Codepage::ASCII),
325 Just(Codepage::CP037),
326 Just(Codepage::CP273),
327 Just(Codepage::CP500),
328 Just(Codepage::CP1047),
329 Just(Codepage::CP1140),
330 ],
331 policy in prop_oneof![Just(ZeroSignPolicy::Positive), Just(ZeroSignPolicy::Preferred)],
332 ) {
333 let encoded = encode_overpunch_byte(digit, is_negative, codepage, policy)
334 .expect("encoding should succeed for digits 0-9");
335
336 prop_assert!(is_valid_overpunch(encoded, codepage));
337
338 let (decoded_digit, decoded_negative) =
339 decode_overpunch_byte(encoded, codepage).expect("decode must succeed for encoded byte");
340
341 prop_assert_eq!(decoded_digit, digit);
342
343 let expected_negative = if codepage.is_ebcdic()
344 && policy == ZeroSignPolicy::Preferred
345 && digit == 0
346 {
347 false
349 } else {
350 is_negative
351 };
352
353 prop_assert_eq!(decoded_negative, expected_negative);
354 }
355 }
356
357 #[test]
358 fn test_ascii_overpunch_encode_decode() {
359 for digit in 0..=9 {
361 let encoded =
362 encode_overpunch_byte(digit, false, Codepage::ASCII, ZeroSignPolicy::Positive)
363 .expect("Failed to encode positive digit");
364 let (decoded_digit, is_negative) = decode_overpunch_byte(encoded, Codepage::ASCII)
365 .expect("Failed to decode positive digit");
366
367 assert_eq!(decoded_digit, digit);
368 assert!(!is_negative);
369 }
370
371 for digit in 0..=9 {
373 let encoded =
374 encode_overpunch_byte(digit, true, Codepage::ASCII, ZeroSignPolicy::Positive)
375 .expect("Failed to encode negative digit");
376 let (decoded_digit, is_negative) = decode_overpunch_byte(encoded, Codepage::ASCII)
377 .expect("Failed to decode negative digit");
378
379 assert_eq!(decoded_digit, digit);
380 assert!(is_negative);
381 }
382 }
383
384 #[test]
385 fn test_ebcdic_overpunch_encode_decode() {
386 for digit in 0..=9 {
388 let encoded =
389 encode_overpunch_byte(digit, false, Codepage::CP037, ZeroSignPolicy::Positive)
390 .expect("Failed to encode positive digit");
391 let (decoded_digit, is_negative) = decode_overpunch_byte(encoded, Codepage::CP037)
392 .expect("Failed to decode positive digit");
393
394 assert_eq!(decoded_digit, digit);
395 assert!(!is_negative);
396
397 let zone = (encoded >> 4) & 0x0F;
399 if digit != 0 {
400 assert_eq!(zone, 0xC);
401 }
402 }
403
404 for digit in 0..=9 {
406 let encoded =
407 encode_overpunch_byte(digit, true, Codepage::CP037, ZeroSignPolicy::Positive)
408 .expect("Failed to encode negative digit");
409 let (decoded_digit, is_negative) = decode_overpunch_byte(encoded, Codepage::CP037)
410 .expect("Failed to decode negative digit");
411
412 assert_eq!(decoded_digit, digit);
413 assert!(is_negative);
414
415 let zone = (encoded >> 4) & 0x0F;
417 assert_eq!(zone, 0xD);
418 }
419 }
420
421 #[test]
422 fn test_zero_sign_policies() {
423 let encoded_pos =
425 encode_overpunch_byte(0, false, Codepage::CP037, ZeroSignPolicy::Positive)
426 .expect("Failed to encode zero with positive policy");
427 let zone_pos = (encoded_pos >> 4) & 0x0F;
428 assert_eq!(zone_pos, 0xC);
429
430 let encoded_pref_pos =
432 encode_overpunch_byte(0, false, Codepage::CP037, ZeroSignPolicy::Preferred)
433 .expect("Failed to encode zero with preferred policy");
434 let zone_pref_pos = (encoded_pref_pos >> 4) & 0x0F;
435 assert_eq!(zone_pref_pos, 0xF);
436
437 let encoded_pref_neg =
438 encode_overpunch_byte(0, true, Codepage::CP037, ZeroSignPolicy::Preferred)
439 .expect("Failed to encode negative zero with preferred policy");
440 let zone_pref_neg = (encoded_pref_neg >> 4) & 0x0F;
441 assert_eq!(zone_pref_neg, 0xF);
442 }
443
444 #[test]
445 fn test_invalid_inputs() {
446 let result = encode_overpunch_byte(10, false, Codepage::ASCII, ZeroSignPolicy::Positive);
448 assert!(result.is_err());
449
450 let result = decode_overpunch_byte(0xFF, Codepage::ASCII);
452 assert!(result.is_err());
453
454 let invalid_ebcdic = 0x1F; let result = decode_overpunch_byte(invalid_ebcdic, Codepage::CP037);
457 assert!(result.is_err());
458 }
459
460 #[test]
461 fn test_ascii_overpunch_golden_values() {
462 assert_eq!(
464 decode_overpunch_byte(b'{', Codepage::ASCII).unwrap(),
465 (0, false)
466 ); assert_eq!(
468 decode_overpunch_byte(b'A', Codepage::ASCII).unwrap(),
469 (1, false)
470 ); assert_eq!(
472 decode_overpunch_byte(b'I', Codepage::ASCII).unwrap(),
473 (9, false)
474 ); assert_eq!(
476 decode_overpunch_byte(b'}', Codepage::ASCII).unwrap(),
477 (0, true)
478 ); assert_eq!(
480 decode_overpunch_byte(b'J', Codepage::ASCII).unwrap(),
481 (1, true)
482 ); assert_eq!(
484 decode_overpunch_byte(b'R', Codepage::ASCII).unwrap(),
485 (9, true)
486 ); }
488
489 #[test]
490 fn test_ebcdic_overpunch_golden_values() {
491 assert_eq!(
493 decode_overpunch_byte(0xC0, Codepage::CP037).unwrap(),
494 (0, false)
495 ); assert_eq!(
497 decode_overpunch_byte(0xC1, Codepage::CP037).unwrap(),
498 (1, false)
499 ); assert_eq!(
501 decode_overpunch_byte(0xC9, Codepage::CP037).unwrap(),
502 (9, false)
503 ); assert_eq!(
505 decode_overpunch_byte(0xD0, Codepage::CP037).unwrap(),
506 (0, true)
507 ); assert_eq!(
509 decode_overpunch_byte(0xD1, Codepage::CP037).unwrap(),
510 (1, true)
511 ); assert_eq!(
513 decode_overpunch_byte(0xD9, Codepage::CP037).unwrap(),
514 (9, true)
515 ); assert_eq!(
517 decode_overpunch_byte(0xF0, Codepage::CP037).unwrap(),
518 (0, false)
519 ); }
521
522 #[test]
523 fn test_is_valid_overpunch() {
524 assert!(is_valid_overpunch(b'A', Codepage::ASCII));
526 assert!(is_valid_overpunch(b'J', Codepage::ASCII));
527 assert!(is_valid_overpunch(b'{', Codepage::ASCII));
528 assert!(is_valid_overpunch(b'0', Codepage::ASCII)); assert!(!is_valid_overpunch(b'Z', Codepage::ASCII));
530
531 assert!(is_valid_overpunch(0xC0, Codepage::CP037));
533 assert!(is_valid_overpunch(0xD9, Codepage::CP037));
534 assert!(is_valid_overpunch(0xF5, Codepage::CP037));
535 assert!(!is_valid_overpunch(0x1A, Codepage::CP037)); assert!(!is_valid_overpunch(0xCA, Codepage::CP037)); }
538
539 #[test]
540 fn test_get_all_valid_overpunch_bytes() {
541 let ascii_bytes = get_all_valid_overpunch_bytes(Codepage::ASCII);
542 assert!(ascii_bytes.len() >= 30); assert!(ascii_bytes.contains(&b'A'));
544 assert!(ascii_bytes.contains(&b'J'));
545 assert!(ascii_bytes.contains(&b'0'));
546
547 let ebcdic_bytes = get_all_valid_overpunch_bytes(Codepage::CP037);
548 assert_eq!(ebcdic_bytes.len(), 30); assert!(ebcdic_bytes.contains(&0xC0));
550 assert!(ebcdic_bytes.contains(&0xD9));
551 assert!(ebcdic_bytes.contains(&0xF5));
552 }
553
554 #[test]
557 fn test_ascii_decode_each_positive_overpunch_char() {
558 let expected: [(u8, u8); 10] = [
559 (b'{', 0),
560 (b'A', 1),
561 (b'B', 2),
562 (b'C', 3),
563 (b'D', 4),
564 (b'E', 5),
565 (b'F', 6),
566 (b'G', 7),
567 (b'H', 8),
568 (b'I', 9),
569 ];
570 for (byte, digit) in expected {
571 let (d, neg) = decode_overpunch_byte(byte, Codepage::ASCII).unwrap();
572 assert_eq!(d, digit, "positive overpunch char 0x{byte:02X}");
573 assert!(
574 !neg,
575 "positive overpunch char 0x{byte:02X} should not be negative"
576 );
577 }
578 }
579
580 #[test]
581 fn test_ascii_decode_each_negative_overpunch_char() {
582 let expected: [(u8, u8); 10] = [
583 (b'}', 0),
584 (b'J', 1),
585 (b'K', 2),
586 (b'L', 3),
587 (b'M', 4),
588 (b'N', 5),
589 (b'O', 6),
590 (b'P', 7),
591 (b'Q', 8),
592 (b'R', 9),
593 ];
594 for (byte, digit) in expected {
595 let (d, neg) = decode_overpunch_byte(byte, Codepage::ASCII).unwrap();
596 assert_eq!(d, digit, "negative overpunch char 0x{byte:02X}");
597 assert!(
598 neg,
599 "negative overpunch char 0x{byte:02X} should be negative"
600 );
601 }
602 }
603
604 #[test]
605 fn test_ascii_encode_each_digit_positive() {
606 let expected_bytes: [u8; 10] = [b'{', b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I'];
607 for digit in 0u8..=9 {
608 let enc =
609 encode_overpunch_byte(digit, false, Codepage::ASCII, ZeroSignPolicy::Positive)
610 .unwrap();
611 assert_eq!(
612 enc, expected_bytes[digit as usize],
613 "encode positive digit {digit}"
614 );
615 }
616 }
617
618 #[test]
619 fn test_ascii_encode_each_digit_negative() {
620 let expected_bytes: [u8; 10] = [b'}', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R'];
621 for digit in 0u8..=9 {
622 let enc = encode_overpunch_byte(digit, true, Codepage::ASCII, ZeroSignPolicy::Positive)
623 .unwrap();
624 assert_eq!(
625 enc, expected_bytes[digit as usize],
626 "encode negative digit {digit}"
627 );
628 }
629 }
630
631 #[test]
632 fn test_all_ebcdic_codepage_variants_encode_decode() {
633 let ebcdic_codepages = [
634 Codepage::CP037,
635 Codepage::CP273,
636 Codepage::CP500,
637 Codepage::CP1047,
638 Codepage::CP1140,
639 ];
640 for cp in ebcdic_codepages {
641 for digit in 0u8..=9 {
642 for is_negative in [false, true] {
643 let enc =
644 encode_overpunch_byte(digit, is_negative, cp, ZeroSignPolicy::Positive)
645 .unwrap_or_else(|e| {
646 panic!("{cp:?} digit={digit} neg={is_negative}: {e}")
647 });
648 let (d, neg) = decode_overpunch_byte(enc, cp)
649 .unwrap_or_else(|e| panic!("{cp:?} byte=0x{enc:02X}: {e}"));
650 assert_eq!(d, digit, "{cp:?} digit={digit} neg={is_negative}");
651 assert_eq!(neg, is_negative, "{cp:?} digit={digit} neg={is_negative}");
652 }
653 }
654 }
655 }
656
657 #[test]
658 fn test_ebcdic_zone_nibbles_all_codepages() {
659 let ebcdic_codepages = [
660 Codepage::CP037,
661 Codepage::CP273,
662 Codepage::CP500,
663 Codepage::CP1047,
664 Codepage::CP1140,
665 ];
666 for cp in ebcdic_codepages {
667 for digit in 0u8..=9 {
668 let pos =
669 encode_overpunch_byte(digit, false, cp, ZeroSignPolicy::Positive).unwrap();
670 assert_eq!(
671 (pos >> 4) & 0x0F,
672 0xC,
673 "{cp:?} positive zone for digit {digit}"
674 );
675 assert_eq!(
676 pos & 0x0F,
677 digit,
678 "{cp:?} positive digit nibble for {digit}"
679 );
680
681 let neg = encode_overpunch_byte(digit, true, cp, ZeroSignPolicy::Positive).unwrap();
682 assert_eq!(
683 (neg >> 4) & 0x0F,
684 0xD,
685 "{cp:?} negative zone for digit {digit}"
686 );
687 assert_eq!(
688 neg & 0x0F,
689 digit,
690 "{cp:?} negative digit nibble for {digit}"
691 );
692 }
693 }
694 }
695
696 #[test]
697 fn test_invalid_ascii_overpunch_bytes() {
698 let invalid_bytes: [u8; 10] = [b'S', b'T', b'Z', b'a', b'z', b'!', b'@', b'#', 0x00, 0xFF];
699 for byte in invalid_bytes {
700 let result = decode_overpunch_byte(byte, Codepage::ASCII);
701 assert!(
702 result.is_err(),
703 "byte 0x{byte:02X} should be invalid ASCII overpunch"
704 );
705 assert!(!is_valid_overpunch(byte, Codepage::ASCII));
706 }
707 }
708
709 #[test]
710 fn test_invalid_ebcdic_overpunch_bytes() {
711 let invalid_zones: [u8; 6] = [0x0, 0x1, 0x2, 0xA, 0xB, 0xE];
713 for zone in invalid_zones {
714 let byte = (zone << 4) | 0x05; let result = decode_overpunch_byte(byte, Codepage::CP037);
716 assert!(
717 result.is_err(),
718 "zone 0x{zone:X} should be invalid for EBCDIC"
719 );
720 assert!(!is_valid_overpunch(byte, Codepage::CP037));
721 }
722 for digit_nibble in 0xAu8..=0xF {
724 let byte = (0xC << 4) | digit_nibble; let result = decode_overpunch_byte(byte, Codepage::CP037);
726 assert!(
727 result.is_err(),
728 "digit nibble 0x{digit_nibble:X} should be invalid"
729 );
730 assert!(!is_valid_overpunch(byte, Codepage::CP037));
731 }
732 }
733
734 #[test]
735 fn test_encode_invalid_digit_rejected() {
736 for digit in [10u8, 11, 15, 99, 255] {
737 let r1 = encode_overpunch_byte(digit, false, Codepage::ASCII, ZeroSignPolicy::Positive);
738 assert!(r1.is_err(), "digit {digit} should be rejected for ASCII");
739 let r2 = encode_overpunch_byte(digit, false, Codepage::CP037, ZeroSignPolicy::Positive);
740 assert!(r2.is_err(), "digit {digit} should be rejected for EBCDIC");
741 }
742 }
743
744 #[test]
745 fn test_ascii_round_trip_all_digit_sign_combinations() {
746 for digit in 0u8..=9 {
747 for is_negative in [false, true] {
748 let enc = encode_overpunch_byte(
749 digit,
750 is_negative,
751 Codepage::ASCII,
752 ZeroSignPolicy::Positive,
753 )
754 .unwrap();
755 let (d, neg) = decode_overpunch_byte(enc, Codepage::ASCII).unwrap();
756 assert_eq!(d, digit);
757 assert_eq!(neg, is_negative);
758 }
759 }
760 }
761
762 #[test]
763 fn test_ebcdic_preferred_zero_round_trip_all_codepages() {
764 let ebcdic_codepages = [
765 Codepage::CP037,
766 Codepage::CP273,
767 Codepage::CP500,
768 Codepage::CP1047,
769 Codepage::CP1140,
770 ];
771 for cp in ebcdic_codepages {
772 let enc_pos = encode_overpunch_byte(0, false, cp, ZeroSignPolicy::Preferred).unwrap();
774 let enc_neg = encode_overpunch_byte(0, true, cp, ZeroSignPolicy::Preferred).unwrap();
775 assert_eq!(enc_pos, 0xF0, "{cp:?} preferred +0");
776 assert_eq!(enc_neg, 0xF0, "{cp:?} preferred -0");
777
778 let (d_pos, neg_pos) = decode_overpunch_byte(enc_pos, cp).unwrap();
779 assert_eq!(d_pos, 0);
780 assert!(!neg_pos, "{cp:?} preferred zero decodes as positive");
781
782 for digit in 1u8..=9 {
784 let enc =
785 encode_overpunch_byte(digit, true, cp, ZeroSignPolicy::Preferred).unwrap();
786 let zone = (enc >> 4) & 0x0F;
787 assert_eq!(
788 zone, 0xD,
789 "{cp:?} preferred policy should not affect digit {digit}"
790 );
791 }
792 }
793 }
794
795 #[test]
796 fn test_positive_and_negative_zero_ascii() {
797 let enc_pos =
799 encode_overpunch_byte(0, false, Codepage::ASCII, ZeroSignPolicy::Positive).unwrap();
800 assert_eq!(enc_pos, b'{');
801 let (d, neg) = decode_overpunch_byte(enc_pos, Codepage::ASCII).unwrap();
802 assert_eq!(d, 0);
803 assert!(!neg);
804
805 let enc_neg =
807 encode_overpunch_byte(0, true, Codepage::ASCII, ZeroSignPolicy::Positive).unwrap();
808 assert_eq!(enc_neg, b'}');
809 let (d, neg) = decode_overpunch_byte(enc_neg, Codepage::ASCII).unwrap();
810 assert_eq!(d, 0);
811 assert!(neg);
812 }
813
814 #[test]
815 fn test_positive_and_negative_zero_ebcdic() {
816 let enc =
818 encode_overpunch_byte(0, false, Codepage::CP037, ZeroSignPolicy::Positive).unwrap();
819 assert_eq!(enc, 0xC0);
820 let (d, neg) = decode_overpunch_byte(enc, Codepage::CP037).unwrap();
821 assert_eq!(d, 0);
822 assert!(!neg);
823
824 let enc =
826 encode_overpunch_byte(0, true, Codepage::CP037, ZeroSignPolicy::Positive).unwrap();
827 assert_eq!(enc, 0xD0);
828 let (d, neg) = decode_overpunch_byte(enc, Codepage::CP037).unwrap();
829 assert_eq!(d, 0);
830 assert!(neg);
831
832 let enc =
834 encode_overpunch_byte(0, true, Codepage::CP037, ZeroSignPolicy::Preferred).unwrap();
835 assert_eq!(enc, 0xF0);
836 let (d, neg) = decode_overpunch_byte(enc, Codepage::CP037).unwrap();
837 assert_eq!(d, 0);
838 assert!(!neg, "preferred zero always decodes as positive");
839 }
840
841 #[test]
842 fn test_ascii_regular_digits_decode_as_unsigned_positive() {
843 for digit in 0u8..=9 {
845 let byte = b'0' + digit;
846 let (d, neg) = decode_overpunch_byte(byte, Codepage::ASCII).unwrap();
847 assert_eq!(d, digit);
848 assert!(!neg, "plain digit {digit} should decode as positive");
849 assert!(is_valid_overpunch(byte, Codepage::ASCII));
850 }
851 }
852
853 #[test]
854 fn test_ebcdic_f_zone_digits_decode_as_positive() {
855 for digit in 0u8..=9 {
857 let byte = 0xF0 | digit;
858 let (d, neg) = decode_overpunch_byte(byte, Codepage::CP037).unwrap();
859 assert_eq!(d, digit);
860 assert!(!neg, "F-zone digit {digit} should decode as positive");
861 }
862 }
863
864 #[test]
865 fn test_boundary_single_digit_encode_decode() {
866 for &(digit, is_neg) in &[(0u8, false), (0, true), (9, false), (9, true)] {
868 let ascii_enc =
869 encode_overpunch_byte(digit, is_neg, Codepage::ASCII, ZeroSignPolicy::Positive)
870 .unwrap();
871 let (d, n) = decode_overpunch_byte(ascii_enc, Codepage::ASCII).unwrap();
872 assert_eq!(d, digit);
873 assert_eq!(n, is_neg);
874
875 let ebcdic_enc =
876 encode_overpunch_byte(digit, is_neg, Codepage::CP037, ZeroSignPolicy::Positive)
877 .unwrap();
878 let (d, n) = decode_overpunch_byte(ebcdic_enc, Codepage::CP037).unwrap();
879 assert_eq!(d, digit);
880 assert_eq!(n, is_neg);
881 }
882 }
883
884 #[test]
885 fn test_ascii_preferred_zero_policy_has_no_effect() {
886 let pos =
888 encode_overpunch_byte(0, false, Codepage::ASCII, ZeroSignPolicy::Preferred).unwrap();
889 let neg =
890 encode_overpunch_byte(0, true, Codepage::ASCII, ZeroSignPolicy::Preferred).unwrap();
891 assert_eq!(pos, b'{', "ASCII preferred +0 should still be '{{' ");
892 assert_eq!(neg, b'}', "ASCII preferred -0 should still be '}}' ");
893 }
894}