Skip to content

Codec

codec

decode_decfloat

decode_decfloat(raw)

Decode a DECFLOAT16 (8B) or DECFLOAT34 (16B) wire value.

Source: tests/golden/serialization/decfloat_response.bin. Never float — that cannot hold a base-10 decimal exactly, which is the whole reason this type exists.

Source code in src/saprfclib/codec.py
def decode_decfloat(raw: bytes) -> Decimal:
    """Decode a DECFLOAT16 (8B) or DECFLOAT34 (16B) wire value.

    Source: tests/golden/serialization/decfloat_response.bin. Never `float` —
    that cannot hold a base-10 decimal exactly, which is the whole reason this
    type exists.
    """
    params = _DECF_PARAMS.get(len(raw))
    if params is None:
        raise ValueError(f"DECFLOAT value must be 8 or 16 bytes, got {len(raw)}")
    econ_bits, declets, bias, _ = params
    # The wire is little-endian; every IEEE field below is defined on the
    # big-endian form, so reverse once here and work in that orientation.
    value = int.from_bytes(raw[::-1], "big")
    total = len(raw) * 8
    sign = (value >> (total - 1)) & 1
    combo = (value >> (total - 6)) & 0x1F
    econ = (value >> (total - 6 - econ_bits)) & ((1 << econ_bits) - 1)

    # Combination field G0..G4 (IEEE 754-2008 section 3.5.2):
    #   11110 -> Infinity, 11111 -> NaN
    #   G0G1 = 11 -> leading digit 8+G4, exponent high bits G2G3
    #   otherwise -> leading digit G2G3G4, exponent high bits G0G1
    if (combo & 0b11110) == 0b11110:
        if combo & 1:
            return Decimal("-NaN") if sign else Decimal("NaN")
        return Decimal("-Infinity") if sign else Decimal("Infinity")
    if (combo >> 3) == 0b11:
        lead, exp_high = 8 + (combo & 1), (combo >> 1) & 0b11
    else:
        lead, exp_high = combo & 0b111, (combo >> 3) & 0b11

    exponent = ((exp_high << econ_bits) | econ) - bias
    coeff = value & ((1 << (total - 6 - econ_bits)) - 1)
    digits = [lead]
    for n in range(declets - 1, -1, -1):
        digits.extend(_DPD_DECODE[(coeff >> (n * 10)) & 0x3FF])
    return Decimal((sign, tuple(digits), exponent))

encode_decfloat

encode_decfloat(value, width)

Encode a value as DECFLOAT16 (width 8) or DECFLOAT34 (width 16).

Round-trips the golden fixture byte-for-byte in both directions.

Source code in src/saprfclib/codec.py
def encode_decfloat(value: Decimal | int | str, width: int) -> bytes:
    """Encode a value as DECFLOAT16 (width 8) or DECFLOAT34 (width 16).

    Round-trips the golden fixture byte-for-byte in both directions.
    """
    params = _DECF_PARAMS.get(width)
    if params is None:
        raise ValueError(f"DECFLOAT width must be 8 or 16, got {width}")
    econ_bits, declets, bias, ndigits = params

    if not isinstance(value, Decimal):
        value = Decimal(value)
    if value.is_nan():
        total = width * 8
        packed = ((1 if value.is_signed() else 0) << (total - 1)) | (0b11111 << (total - 6))
        return packed.to_bytes(width, "big")[::-1]
    if value.is_infinite():
        total = width * 8
        packed = ((1 if value.is_signed() else 0) << (total - 1)) | (0b11110 << (total - 6))
        return packed.to_bytes(width, "big")[::-1]

    sign, digits, exponent = value.as_tuple()
    if not isinstance(exponent, int):  # pragma: no cover - guarded by is_nan/is_infinite
        raise ValueError(f"cannot encode special Decimal {value!r} as DECFLOAT")
    if len(digits) > ndigits:
        raise ValueError(
            f"{value} has {len(digits)} significant digits; DECFLOAT{ndigits} holds {ndigits}"
        )
    biased = exponent + bias
    if not 0 <= biased <= (3 << econ_bits) - 1:
        raise ValueError(f"exponent {exponent} is outside the DECFLOAT{ndigits} range")

    padded = (0,) * (ndigits - len(digits)) + tuple(digits)
    lead = padded[0]
    exp_high = biased >> econ_bits
    if lead <= 7:
        combo = (exp_high << 3) | lead
    else:
        combo = 0b11000 | (exp_high << 1) | (lead & 1)
    coeff = 0
    for n in range(declets):
        trio = padded[1 + n * 3 : 4 + n * 3]
        coeff = (coeff << 10) | _DPD_ENCODE[(trio[0], trio[1], trio[2])]

    total = width * 8
    packed = (
        (sign << (total - 1))
        | (combo << (total - 6))
        | ((biased & ((1 << econ_bits) - 1)) << (total - 6 - econ_bits))
        | coeff
    )
    return packed.to_bytes(width, "big")[::-1]

decode

decode(rfctype, data, field)

Decode wire bytes into a Python value for the given RFCTYPE.

Parameters:

Name Type Description Default
rfctype int

The RFCTYPE constant identifying the ABAP data type (RFCTYPE_CHAR, RFCTYPE_INT, etc.). See saprfclib.types for the full set of constants.

required
data bytes | bytearray | memoryview

Raw wire bytes to decode. All three buffer types are accepted without copying (CODEC-06).

required
field FieldDesc

Descriptor carrying unicode_mode, nuc_offset/ nuc_length, uc_offset/uc_length, decimals, and type_desc. Used for STRUCTURE/TABLE layout and BCD precision.

required

Returns:

Type Description
Any

Python-native type corresponding to the RFCTYPE — str for

Any

CHAR/NUM/DATE/TIME/STRING, bytes for BYTE/XSTRING, int for

Any

INT1/INT2/INT4/INT8 and all temporal extension types, float for FLOAT,

Any

decimal.Decimal for BCD, dict for STRUCTURE, list[dict] for TABLE.

Raises:

Type Description
ValueError

If rfctype is out-of-scope or unknown.

NotImplementedError

For deferred types.

Source code in src/saprfclib/codec.py
def decode(rfctype: int, data: bytes | bytearray | memoryview, field: FieldDesc) -> Any:
    """Decode wire bytes into a Python value for the given RFCTYPE.

    Args:
        rfctype (int): The RFCTYPE constant identifying the ABAP data type
            (RFCTYPE_CHAR, RFCTYPE_INT, etc.). See saprfclib.types for the full
            set of constants.
        data (bytes | bytearray | memoryview): Raw wire bytes to decode. All
            three buffer types are accepted without copying (CODEC-06).
        field (FieldDesc): Descriptor carrying unicode_mode, nuc_offset/
            nuc_length, uc_offset/uc_length, decimals, and type_desc. Used for
            STRUCTURE/TABLE layout and BCD precision.

    Returns:
        Python-native type corresponding to the RFCTYPE — str for
        CHAR/NUM/DATE/TIME/STRING, bytes for BYTE/XSTRING, int for
        INT1/INT2/INT4/INT8 and all temporal extension types, float for FLOAT,
        decimal.Decimal for BCD, dict for STRUCTURE, list[dict] for TABLE.

    Raises:
        ValueError: If rfctype is out-of-scope or unknown.
        NotImplementedError: For deferred types.
    """
    buf = _as_bytes(data)

    if rfctype in _OUT_OF_SCOPE:
        raise ValueError(f"unsupported RFCTYPE {rfctype}")
    if rfctype in _DEFERRED:
        raise NotImplementedError(
            f"RFCTYPE {rfctype} decode not yet implemented — see {_DEFERRED[rfctype]}"
        )
    if rfctype in (RFCTYPE_DECF16, RFCTYPE_DECF34):
        return decode_decfloat(bytes(buf))

    match rfctype:
        case _ if rfctype in _INT_FORMATS:
            fmt = _INT_FORMATS[rfctype]
            (value,) = struct.unpack(fmt, buf[: struct.calcsize(fmt)])
            return value
        case rfctype if rfctype == RFCTYPE_BCD:
            return _decode_bcd(buf, field)
        case rfctype if rfctype == RFCTYPE_INT1:
            return buf[0]  # unsigned single byte
        case rfctype if rfctype == RFCTYPE_FLOAT:
            (value,) = struct.unpack("<d", buf[:8])
            return value
        case rfctype if rfctype == RFCTYPE_CHAR:
            return _decode_uc_fixed(buf, field).rstrip(" ")
        case rfctype if rfctype == RFCTYPE_NUM:
            return _decode_uc_fixed(buf, field)
        case rfctype if rfctype == RFCTYPE_DATE:
            return _decode_uc_fixed(buf, field)  # str "YYYYMMDD" — NOT datetime (D-13)
        case rfctype if rfctype == RFCTYPE_TIME:
            return _decode_uc_fixed(buf, field)  # str "HHMMSS" — NOT time (D-13)
        case rfctype if rfctype == RFCTYPE_BYTE:
            return buf
        case rfctype if rfctype == RFCTYPE_STRING:
            return buf.decode("utf-8")
        case rfctype if rfctype == RFCTYPE_XSTRING:
            return buf
        case rfctype if rfctype == RFCTYPE_STRUCTURE:
            return _decode_structure(buf, _require_type_desc(field, "decode"), field.unicode_mode)
        case rfctype if rfctype == RFCTYPE_TABLE:
            return _decode_table(buf, field)
        case _:
            raise ValueError(f"unknown RFCTYPE {rfctype}")

encode

encode(rfctype, value, field)

Encode a Python value into wire bytes for the given RFCTYPE.

Parameters:

Name Type Description Default
rfctype int

The RFCTYPE constant identifying the ABAP data type.

required
value Any

Python-native value to encode. Type must match the RFCTYPE: str for character types, int for integer types, decimal.Decimal for BCD, bytes for BYTE/XSTRING, dict for STRUCTURE, list[dict] for TABLE.

required
field FieldDesc

Descriptor carrying layout, precision, and type_desc. Same as decode().

required

Returns:

Name Type Description
bytes bytes

The wire representation of value for this RFCTYPE and field

bytes

descriptor.

Raises:

Type Description
ValueError

For out-of-scope or unknown rfctype.

NotImplementedError

For deferred types.

ValueError

For a DECFLOAT value that does not fit the target width.

TypeError

If value is the wrong Python type for the rfctype.

Source code in src/saprfclib/codec.py
def encode(rfctype: int, value: Any, field: FieldDesc) -> bytes:
    """Encode a Python value into wire bytes for the given RFCTYPE.

    Args:
        rfctype (int): The RFCTYPE constant identifying the ABAP data type.
        value (Any): Python-native value to encode. Type must match the
            RFCTYPE: str for character types, int for integer types,
            decimal.Decimal for BCD, bytes for BYTE/XSTRING, dict for
            STRUCTURE, list[dict] for TABLE.
        field (FieldDesc): Descriptor carrying layout, precision, and
            type_desc. Same as decode().

    Returns:
        bytes: The wire representation of value for this RFCTYPE and field
        descriptor.

    Raises:
        ValueError: For out-of-scope or unknown rfctype.
        NotImplementedError: For deferred types.
        ValueError: For a DECFLOAT value that does not fit the target width.
        TypeError: If value is the wrong Python type for the rfctype.
    """
    if rfctype in _OUT_OF_SCOPE:
        raise ValueError(f"unsupported RFCTYPE {rfctype}")
    if rfctype in _DEFERRED:
        raise NotImplementedError(
            f"RFCTYPE {rfctype} encode not yet implemented — see {_DEFERRED[rfctype]}"
        )
    if rfctype in (RFCTYPE_DECF16, RFCTYPE_DECF34):
        return encode_decfloat(
            cast("Decimal | int | str", value), 8 if rfctype == RFCTYPE_DECF16 else 16
        )

    match rfctype:
        case _ if rfctype in _INT_FORMATS:
            return struct.pack(_INT_FORMATS[rfctype], int(value))
        case rfctype if rfctype == RFCTYPE_BCD:
            return _encode_bcd(value, field)
        case rfctype if rfctype == RFCTYPE_INT1:
            iv = int(value)
            if not 0 <= iv <= 0xFF:
                raise ValueError(f"INT1 out of range: {iv}")
            return bytes((iv,))
        case rfctype if rfctype == RFCTYPE_FLOAT:
            return struct.pack("<d", float(value))
        case rfctype if rfctype == RFCTYPE_CHAR:
            return _encode_uc_fixed(value, field, pad=" ")
        case rfctype if rfctype == RFCTYPE_NUM:
            return _encode_uc_fixed(value, field, pad="0")
        case rfctype if rfctype == RFCTYPE_DATE:
            return _encode_uc_fixed(value, field, pad=" ")
        case rfctype if rfctype == RFCTYPE_TIME:
            return _encode_uc_fixed(value, field, pad=" ")
        case rfctype if rfctype == RFCTYPE_BYTE:
            return bytes(value)
        case rfctype if rfctype == RFCTYPE_STRING:
            return cast(bytes, value.encode("utf-8"))
        case rfctype if rfctype == RFCTYPE_XSTRING:
            return bytes(value)
        case rfctype if rfctype == RFCTYPE_STRUCTURE:
            return _encode_structure(value, _require_type_desc(field, "encode"), field.unicode_mode)
        case rfctype if rfctype == RFCTYPE_TABLE:
            return _encode_table(value, field)
        case _:
            raise ValueError(f"unknown RFCTYPE {rfctype}")