overpunch/
lib.rs

1use rust_decimal::prelude::ToPrimitive;
2use rust_decimal::Decimal;
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum OverpunchError {
7    #[error("cannot extract from an empty field")]
8    EmptyField,
9
10    #[error("failed to parse result as decimal: {0}")]
11    ParseError(String),
12
13    #[error("failed with overflow while serializing value: {0}")]
14    OverflowError(String),
15}
16
17/// Returns a `str` serialized from a `Decimal` to the appropriate signed overpunch respresentation.
18///
19/// # Arguments
20///
21/// * `value` - The `Decimal` value to serialize.
22/// * `field_format` - The signed overpunch picture format.
23///
24/// # Example
25///
26/// ```
27/// # use overpunch::convert_to_signed_format;
28/// # use rust_decimal::Decimal;
29///
30/// let formatted = convert_to_signed_format(Decimal::from_str_exact("225.8").unwrap(), "s9(7)v99").unwrap();
31/// assert_eq!(formatted, "2258{");
32/// ```
33pub fn convert_to_signed_format(value: Decimal, field_format: &str) -> Option<String> {
34    let number_of_decimal_places = if let Some(pos) = field_format.find('v') {
35        field_format[pos + 1..].len()
36    } else {
37        0
38    };
39
40    format(value, number_of_decimal_places).ok()
41}
42
43/// Returns a `Decimal` parsed from an appropriate signed overpunch respresentation.
44///
45/// # Arguments
46///
47/// * `value` - The signed overpunch representation.
48/// * `field_format` - The signed overpunch picture format.
49///
50/// # Example
51///
52/// ```
53/// # use overpunch::convert_from_signed_format;
54/// # use rust_decimal::Decimal;
55///
56/// let number = convert_from_signed_format("2258{", "s9(7)v99").unwrap();
57/// assert_eq!(number, Decimal::from_str_exact("225.8").unwrap());
58/// ```
59pub fn convert_from_signed_format(value: &str, field_format: &str) -> Option<Decimal> {
60    let number_of_decimal_places = if let Some(pos) = field_format.find('v') {
61        field_format[pos + 1..].len()
62    } else {
63        0
64    };
65
66    extract(value, number_of_decimal_places).ok()
67}
68
69/// Returns a `Decimal` parsed from an appropriate signed overpunch respresentation.
70///
71/// # Arguments
72///
73/// * `value` - The signed overpunch representation.
74/// * `decimals` - The number of digits following the decimal point that this value has.
75///
76/// # Example
77///
78/// ```
79/// # use overpunch::extract;
80/// # use rust_decimal::Decimal;
81///
82/// let number = extract("2258{", 2).unwrap();
83/// assert_eq!(number, Decimal::from_str_exact("225.8").unwrap());
84/// ```
85pub fn extract(raw: &str, decimals: usize) -> Result<Decimal, OverpunchError> {
86    let length = raw.len();
87    if length == 0 {
88        return Err(OverpunchError::EmptyField);
89    }
90
91    let mut val: i64 = 0;
92
93    let mut sign: i64 = 1;
94
95    for c in raw.chars() {
96        let char_val: i64 = match c {
97            '0' => 0,
98            '1' => 1,
99            '2' => 2,
100            '3' => 3,
101            '4' => 4,
102            '5' => 5,
103            '6' => 6,
104            '7' => 7,
105            '8' => 8,
106            '9' => 9,
107            '{' => 0,
108            'A' => 1,
109            'B' => 2,
110            'C' => 3,
111            'D' => 4,
112            'E' => 5,
113            'F' => 6,
114            'G' => 7,
115            'H' => 8,
116            'I' => 9,
117            '}' => 0,
118            'J' => 1,
119            'K' => 2,
120            'L' => 3,
121            'M' => 4,
122            'N' => 5,
123            'O' => 6,
124            'P' => 7,
125            'Q' => 8,
126            'R' => 9,
127            _ => return Err(OverpunchError::ParseError(raw.to_string())),
128        };
129
130        sign = match c {
131            '}' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' => -1,
132            _ => 1,
133        };
134
135        val = val * 10 + char_val;
136    }
137
138    let extracted = if sign == -1 {
139        -Decimal::new(val, decimals as u32)
140    } else {
141        Decimal::new(val, decimals as u32)
142    };
143
144    Ok(extracted)
145}
146
147/// Returns a `str` serialized from a `Decimal` to the appropriate signed overpunch respresentation.
148///
149/// # Arguments
150///
151/// * `value` - The `Decimal` value to serialize.
152/// * `decimals` - The number of digits following the decimal point that the signed overpunch
153///   picture implies.
154///
155/// # Example
156///
157/// ```
158/// # use overpunch::format;
159/// # use rust_decimal::Decimal;
160///
161/// let formatted = format(Decimal::from_str_exact("225.8").unwrap(), 2).unwrap();
162/// assert_eq!(formatted, "2258{");
163/// ```
164pub fn format(value: Decimal, decimals: usize) -> Result<String, OverpunchError> {
165    let is_negative: bool = value.is_sign_negative();
166
167    let scale_factor: Decimal = Decimal::new(10_i64.pow(decimals.try_into().unwrap()), 0);
168
169    let mut working_value = value.abs();
170    working_value.rescale(decimals.try_into().unwrap());
171
172    let mut as_int: i64 = match (working_value * scale_factor).to_i64() {
173        Some(valid_i64) => valid_i64,
174        None => return Err(OverpunchError::OverflowError(value.to_string())),
175    };
176
177    let mut v: Vec<char> = Vec::with_capacity(10);
178
179    let mut last_digit = as_int % 10;
180    as_int /= 10;
181
182    let mut c = match (is_negative, last_digit) {
183        (false, 0) => '{',
184        (false, 1) => 'A',
185        (false, 2) => 'B',
186        (false, 3) => 'C',
187        (false, 4) => 'D',
188        (false, 5) => 'E',
189        (false, 6) => 'F',
190        (false, 7) => 'G',
191        (false, 8) => 'H',
192        (false, 9) => 'I',
193        (true, 0) => '}',
194        (true, 1) => 'J',
195        (true, 2) => 'K',
196        (true, 3) => 'L',
197        (true, 4) => 'M',
198        (true, 5) => 'N',
199        (true, 6) => 'O',
200        (true, 7) => 'P',
201        (true, 8) => 'Q',
202        (true, 9) => 'R',
203        _ => unreachable!(),
204    };
205
206    v.push(c);
207
208    while as_int > 0 {
209        last_digit = as_int % 10;
210        as_int /= 10;
211
212        c = match last_digit {
213            0 => '0',
214            1 => '1',
215            2 => '2',
216            3 => '3',
217            4 => '4',
218            5 => '5',
219            6 => '6',
220            7 => '7',
221            8 => '8',
222            9 => '9',
223            _ => unreachable!(),
224        };
225
226        v.push(c);
227    }
228
229    while v.len() < decimals + 1 {
230        v.push('0');
231    }
232
233    v.reverse();
234
235    Ok(String::from_iter(v))
236}