Skip to main content

copybook_codec/numeric/
float.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! COMP-1 and COMP-2 floating-point codecs.
3
4use crate::options::FloatFormat;
5use copybook_core::{Error, ErrorCode, Result};
6
7const IBM_HEX_EXPONENT_BIAS: i32 = 64;
8const IBM_HEX_FRACTION_MIN: f64 = 1.0 / 16.0;
9const IBM_HEX_SINGLE_FRACTION_BITS: u32 = 24;
10const IBM_HEX_DOUBLE_FRACTION_BITS: u32 = 56;
11
12#[inline]
13fn validate_float_buffer_len(data: &[u8], required: usize, usage: &str) -> Result<()> {
14    if data.len() < required {
15        return Err(Error::new(
16            ErrorCode::CBKD301_RECORD_TOO_SHORT,
17            format!("{usage} requires {required} bytes, got {}", data.len()),
18        ));
19    }
20    Ok(())
21}
22
23#[inline]
24fn validate_float_encode_buffer_len(buffer: &[u8], required: usize, usage: &str) -> Result<()> {
25    if buffer.len() < required {
26        return Err(Error::new(
27            ErrorCode::CBKE510_NUMERIC_OVERFLOW,
28            format!(
29                "{usage} requires {required} bytes, buffer has {}",
30                buffer.len()
31            ),
32        ));
33    }
34    Ok(())
35}
36
37#[inline]
38#[allow(clippy::cast_precision_loss)]
39fn decode_ibm_hex_to_f64(sign: bool, exponent_raw: u8, fraction_bits: u64, bits: u32) -> f64 {
40    if exponent_raw == 0 && fraction_bits == 0 {
41        return if sign { -0.0 } else { 0.0 };
42    }
43
44    let exponent = i32::from(exponent_raw) - IBM_HEX_EXPONENT_BIAS;
45    let divisor = 2_f64.powi(i32::try_from(bits).unwrap_or(0));
46    let fraction = (fraction_bits as f64) / divisor;
47    let magnitude = fraction * 16_f64.powi(exponent);
48    if sign { -magnitude } else { magnitude }
49}
50
51#[inline]
52#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
53fn encode_f64_to_ibm_hex_parts(value: f64, bits: u32) -> Result<(u8, u64)> {
54    if !value.is_finite() {
55        return Err(Error::new(
56            ErrorCode::CBKE510_NUMERIC_OVERFLOW,
57            "IBM hex float encoding requires finite values",
58        ));
59    }
60
61    if value == 0.0 {
62        return Ok((0, 0));
63    }
64
65    let mut exponent = IBM_HEX_EXPONENT_BIAS;
66    let mut fraction = value.abs();
67
68    while fraction < IBM_HEX_FRACTION_MIN {
69        fraction *= 16.0;
70        exponent -= 1;
71        if exponent <= 0 {
72            return Ok((0, 0));
73        }
74    }
75
76    while fraction >= 1.0 {
77        fraction /= 16.0;
78        exponent += 1;
79        if exponent >= 128 {
80            return Err(Error::new(
81                ErrorCode::CBKE510_NUMERIC_OVERFLOW,
82                "IBM hex float exponent overflow",
83            ));
84        }
85    }
86
87    let scale = 2_f64.powi(i32::try_from(bits).unwrap_or(0));
88    let mut fraction_bits = (fraction * scale).round() as u64;
89    let full_scale = 1_u64 << bits;
90    if fraction_bits >= full_scale {
91        // Carry from rounding: re-normalize to 0x1... and bump exponent.
92        fraction_bits = 1_u64 << (bits - 4);
93        exponent += 1;
94        if exponent >= 128 {
95            return Err(Error::new(
96                ErrorCode::CBKE510_NUMERIC_OVERFLOW,
97                "IBM hex float exponent overflow",
98            ));
99        }
100    }
101
102    Ok((u8::try_from(exponent).unwrap_or(0), fraction_bits))
103}
104
105/// Decode a COMP-1 field in IEEE-754 big-endian format.
106///
107/// # Errors
108/// Returns `CBKD301_RECORD_TOO_SHORT` if the data slice has fewer than 4 bytes.
109#[inline]
110#[must_use = "Handle the Result or propagate the error"]
111pub fn decode_float_single_ieee_be(data: &[u8]) -> Result<f32> {
112    validate_float_buffer_len(data, 4, "COMP-1")?;
113    let bytes: [u8; 4] = [data[0], data[1], data[2], data[3]];
114    Ok(f32::from_be_bytes(bytes))
115}
116
117/// Decode a COMP-2 field in IEEE-754 big-endian format.
118///
119/// # Errors
120/// Returns `CBKD301_RECORD_TOO_SHORT` if the data slice has fewer than 8 bytes.
121#[inline]
122#[must_use = "Handle the Result or propagate the error"]
123pub fn decode_float_double_ieee_be(data: &[u8]) -> Result<f64> {
124    validate_float_buffer_len(data, 8, "COMP-2")?;
125    let bytes: [u8; 8] = [
126        data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
127    ];
128    Ok(f64::from_be_bytes(bytes))
129}
130
131/// Decode a COMP-1 field in IBM hexadecimal floating-point format.
132///
133/// # Errors
134/// Returns `CBKD301_RECORD_TOO_SHORT` if the data slice has fewer than 4 bytes.
135#[inline]
136#[must_use = "Handle the Result or propagate the error"]
137pub fn decode_float_single_ibm_hex(data: &[u8]) -> Result<f32> {
138    validate_float_buffer_len(data, 4, "COMP-1")?;
139    let word = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
140    let sign = (word & 0x8000_0000) != 0;
141    let exponent_raw = ((word >> 24) & 0x7F) as u8;
142    let fraction_bits = u64::from(word & 0x00FF_FFFF);
143    let value = decode_ibm_hex_to_f64(
144        sign,
145        exponent_raw,
146        fraction_bits,
147        IBM_HEX_SINGLE_FRACTION_BITS,
148    );
149    #[allow(clippy::cast_possible_truncation)]
150    {
151        Ok(value as f32)
152    }
153}
154
155/// Decode a COMP-2 field in IBM hexadecimal floating-point format.
156///
157/// # Errors
158/// Returns `CBKD301_RECORD_TOO_SHORT` if the data slice has fewer than 8 bytes.
159#[inline]
160#[must_use = "Handle the Result or propagate the error"]
161pub fn decode_float_double_ibm_hex(data: &[u8]) -> Result<f64> {
162    validate_float_buffer_len(data, 8, "COMP-2")?;
163    let word = u64::from_be_bytes([
164        data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
165    ]);
166    let sign = (word & 0x8000_0000_0000_0000) != 0;
167    let exponent_raw = ((word >> 56) & 0x7F) as u8;
168    let fraction_bits = word & 0x00FF_FFFF_FFFF_FFFF;
169    Ok(decode_ibm_hex_to_f64(
170        sign,
171        exponent_raw,
172        fraction_bits,
173        IBM_HEX_DOUBLE_FRACTION_BITS,
174    ))
175}
176
177/// Decode a COMP-1 float with explicit format selection.
178///
179/// # Errors
180/// Returns format-specific decode errors.
181#[inline]
182#[must_use = "Handle the Result or propagate the error"]
183pub fn decode_float_single_with_format(data: &[u8], format: FloatFormat) -> Result<f32> {
184    match format {
185        FloatFormat::IeeeBigEndian => decode_float_single_ieee_be(data),
186        FloatFormat::IbmHex => decode_float_single_ibm_hex(data),
187    }
188}
189
190/// Decode a COMP-2 float with explicit format selection.
191///
192/// # Errors
193/// Returns format-specific decode errors.
194#[inline]
195#[must_use = "Handle the Result or propagate the error"]
196pub fn decode_float_double_with_format(data: &[u8], format: FloatFormat) -> Result<f64> {
197    match format {
198        FloatFormat::IeeeBigEndian => decode_float_double_ieee_be(data),
199        FloatFormat::IbmHex => decode_float_double_ibm_hex(data),
200    }
201}
202
203/// Decode a COMP-1 float with default IEEE-754 big-endian interpretation.
204///
205/// # Errors
206/// Returns `CBKD301_RECORD_TOO_SHORT` if the data slice has fewer than 4 bytes.
207#[inline]
208#[must_use = "Handle the Result or propagate the error"]
209pub fn decode_float_single(data: &[u8]) -> Result<f32> {
210    decode_float_single_ieee_be(data)
211}
212
213/// Decode a COMP-2 float with default IEEE-754 big-endian interpretation.
214///
215/// # Errors
216/// Returns `CBKD301_RECORD_TOO_SHORT` if the data slice has fewer than 8 bytes.
217#[inline]
218#[must_use = "Handle the Result or propagate the error"]
219pub fn decode_float_double(data: &[u8]) -> Result<f64> {
220    decode_float_double_ieee_be(data)
221}
222
223/// Encode a COMP-1 value in IEEE-754 big-endian format.
224///
225/// # Errors
226/// Returns `CBKE510_NUMERIC_OVERFLOW` if the buffer has fewer than 4 bytes.
227#[inline]
228#[must_use = "Handle the Result or propagate the error"]
229pub fn encode_float_single_ieee_be(value: f32, buffer: &mut [u8]) -> Result<()> {
230    validate_float_encode_buffer_len(buffer, 4, "COMP-1")?;
231    let bytes = value.to_be_bytes();
232    buffer[..4].copy_from_slice(&bytes);
233    Ok(())
234}
235
236/// Encode a COMP-2 value in IEEE-754 big-endian format.
237///
238/// # Errors
239/// Returns `CBKE510_NUMERIC_OVERFLOW` if the buffer has fewer than 8 bytes.
240#[inline]
241#[must_use = "Handle the Result or propagate the error"]
242pub fn encode_float_double_ieee_be(value: f64, buffer: &mut [u8]) -> Result<()> {
243    validate_float_encode_buffer_len(buffer, 8, "COMP-2")?;
244    let bytes = value.to_be_bytes();
245    buffer[..8].copy_from_slice(&bytes);
246    Ok(())
247}
248
249/// Encode a COMP-1 value in IBM hexadecimal floating-point format.
250///
251/// # Errors
252/// Returns:
253/// - `CBKE510_NUMERIC_OVERFLOW` if the buffer is too small
254/// - `CBKE510_NUMERIC_OVERFLOW` for non-finite values or exponent overflow
255#[inline]
256#[must_use = "Handle the Result or propagate the error"]
257pub fn encode_float_single_ibm_hex(value: f32, buffer: &mut [u8]) -> Result<()> {
258    validate_float_encode_buffer_len(buffer, 4, "COMP-1")?;
259    let sign = value.is_sign_negative();
260    let (exponent_raw, fraction_bits) =
261        encode_f64_to_ibm_hex_parts(f64::from(value), IBM_HEX_SINGLE_FRACTION_BITS)?;
262    let sign_bit = if sign { 0x8000_0000 } else { 0 };
263    let fraction_low = u32::try_from(fraction_bits & 0x00FF_FFFF).map_err(|_| {
264        Error::new(
265            ErrorCode::CBKE510_NUMERIC_OVERFLOW,
266            "IBM hex fraction overflow for COMP-1",
267        )
268    })?;
269    let word = sign_bit | (u32::from(exponent_raw) << 24) | fraction_low;
270    buffer[..4].copy_from_slice(&word.to_be_bytes());
271    Ok(())
272}
273
274/// Encode a COMP-2 value in IBM hexadecimal floating-point format.
275///
276/// # Errors
277/// Returns:
278/// - `CBKE510_NUMERIC_OVERFLOW` if the buffer is too small
279/// - `CBKE510_NUMERIC_OVERFLOW` for non-finite values or exponent overflow
280#[inline]
281#[must_use = "Handle the Result or propagate the error"]
282pub fn encode_float_double_ibm_hex(value: f64, buffer: &mut [u8]) -> Result<()> {
283    validate_float_encode_buffer_len(buffer, 8, "COMP-2")?;
284    let sign = value.is_sign_negative();
285    let (exponent_raw, fraction_bits) =
286        encode_f64_to_ibm_hex_parts(value, IBM_HEX_DOUBLE_FRACTION_BITS)?;
287    let sign_bit = if sign { 0x8000_0000_0000_0000 } else { 0 };
288    let word = sign_bit | (u64::from(exponent_raw) << 56) | (fraction_bits & 0x00FF_FFFF_FFFF_FFFF);
289    buffer[..8].copy_from_slice(&word.to_be_bytes());
290    Ok(())
291}
292
293/// Encode a COMP-1 float with explicit format selection.
294///
295/// # Errors
296/// Returns format-specific encode errors.
297#[inline]
298#[must_use = "Handle the Result or propagate the error"]
299pub fn encode_float_single_with_format(
300    value: f32,
301    buffer: &mut [u8],
302    format: FloatFormat,
303) -> Result<()> {
304    match format {
305        FloatFormat::IeeeBigEndian => encode_float_single_ieee_be(value, buffer),
306        FloatFormat::IbmHex => encode_float_single_ibm_hex(value, buffer),
307    }
308}
309
310/// Encode a COMP-2 float with explicit format selection.
311///
312/// # Errors
313/// Returns format-specific encode errors.
314#[inline]
315#[must_use = "Handle the Result or propagate the error"]
316pub fn encode_float_double_with_format(
317    value: f64,
318    buffer: &mut [u8],
319    format: FloatFormat,
320) -> Result<()> {
321    match format {
322        FloatFormat::IeeeBigEndian => encode_float_double_ieee_be(value, buffer),
323        FloatFormat::IbmHex => encode_float_double_ibm_hex(value, buffer),
324    }
325}
326
327/// Encode a COMP-1 float with default IEEE-754 big-endian format.
328///
329/// # Errors
330/// Returns `CBKE510_NUMERIC_OVERFLOW` if the buffer has fewer than 4 bytes.
331#[inline]
332#[must_use = "Handle the Result or propagate the error"]
333pub fn encode_float_single(value: f32, buffer: &mut [u8]) -> Result<()> {
334    encode_float_single_ieee_be(value, buffer)
335}
336
337/// Encode a COMP-2 float with default IEEE-754 big-endian format.
338///
339/// # Errors
340/// Returns `CBKE510_NUMERIC_OVERFLOW` if the buffer has fewer than 8 bytes.
341#[inline]
342#[must_use = "Handle the Result or propagate the error"]
343pub fn encode_float_double(value: f64, buffer: &mut [u8]) -> Result<()> {
344    encode_float_double_ieee_be(value, buffer)
345}