Subtract¶
Level: 201 · for anyone with a hex editor open
One line: X[i] -= Operand wraps below zero the way Add wraps past the top, so zero minus one is every bit set in every type, Subtract 1 writes exactly what Add 0xFF writes, and Subtract 0x20 uppercases ASCII letters while turning a space into NUL.
What the dialog does¶
The manual writes Subtract as X[i] -= Operand (Hex Operations ↗). Everything Add says about widths carries over in the other direction: a borrow travels up through the bytes of one value and stops at its edge, so the same Subtract on the same two bytes writes three different results as a Byte, a little-endian Short and a big-endian Short (section 1).
In Python¶
Verified output of subtract_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. THE SAME BYTES, THREE DIFFERENCES
------------------------------------------------------------------------
Subtract 0x10 from 08 01:
Unsigned Byte f8 f1 each byte borrows from nowhere
Unsigned Short, little f8 00 0108-10 is 00F8
Unsigned Short, big 07 f1 0801-10 is 07F1
2. ZERO MINUS ONE IS EVERY BIT SET, IN EVERY TYPE
------------------------------------------------------------------------
Subtract 1 from zero bytes, then read the result back as the same
type:
Unsigned Byte ff 255
Signed Byte ff -1
Unsigned Short ff ff 65535
Signed Int ff ff ff ff -1
Unsigned Int64 ff ff ff ff ff ff ff ff 18446744073709551615
The bytes are the same in each signed and unsigned pair. The
number is not, and the file only ever holds the bytes.
3. SUBTRACT 1 IS ADD FF
------------------------------------------------------------------------
Wrapping makes subtraction a special case of addition, which is why
a dialog could offer only one of them:
Subtract 1 == Add 0xFF, all 256 Unsigned Bytes True
Subtract 1 == Add 0xFFFF, all 65,536 Unsigned Shorts True
4. SUBTRACT 20 ON A LINE OF TEXT
------------------------------------------------------------------------
Lowercase ASCII sits exactly 0x20 above uppercase, so Subtract 0x20
as Unsigned Byte uppercases letters -- and does the same arithmetic
to every other byte:
before 63 61 66 c3 a9 20 34 32 'café 42'
after 43 41 46 a3 89 00 14 12 'CAF��\x00\x14\x12'
The letters came out right. The space became NUL, the digits became
control characters, and C3 A9 became A3 89 -- a continuation byte
where a lead byte has to be, so the é is two replacement characters.
Zero minus one¶
Section 2 subtracts 1 from zero in five types and gets all ones every time — FF, FF FF, four FFs, eight. Only the reading differs: 255 or −1 for the same byte, 65,535 for the Short, 18,446,744,073,709,551,615 for the Unsigned Int64. The file never holds a reading, only bytes, which is why an inspector pane beside a hex view shows signed and unsigned columns for the same bytes rather than choosing between them.
Subtract is Add, wrapped¶
Section 3 checks Subtract 1 against Add 0xFF on every Unsigned Byte, and Subtract 1 against Add 0xFFFF on every Unsigned Short, and gets the same bytes all 256 and all 65,536 times. Once values wrap, subtracting k and adding 2ⁿ − k are one operation. Negate is the same idea applied to zero.
Subtract 20 on text¶
ASCII puts every lowercase letter exactly 0x20 above its capital, so Subtract 0x20 as Unsigned Byte is an uppercase, for letters. Section 4 runs it on café 42 and shows the rest of the bill: the space, 20, became NUL; the digits 34 32 became the control characters 14 12; and the é, C3 A9, became A3 89, a continuation byte standing where UTF-8 needs a lead byte, so a decoder reports two errors. Arithmetic does not know which bytes are letters. Binary And and Binary Or change case by clearing or setting a single bit instead, and leave far more of a line alone.
What the manual does not say¶
- What a result below the bottom of a signed type becomes. The programs keep the low bits.
- What becomes of a last value too short for the type.
If you are coming from Python or ABAP¶
Python. (b - k) & 0xFF is the whole of Subtract as Unsigned Byte, and the mask is not optional: Python's −1 has no width, and bytes([-1]) raises ValueError rather than writing FF. To read a result as signed, struct.unpack('b', ...) or int.from_bytes(..., signed=True) — Bytes, hex and int has the four conversions.
ABAP. (Not machine-checked — CI cannot run ABAP.) An integer subtraction whose result leaves the type raises CX_SY_ARITHMETIC_OVERFLOW ↗, and i is a signed type, so 0 - 1 is −1 and never 4,294,967,295. The all-ones bytes come from the assignment instead: −1 assigned to a TYPE x LENGTH 4 field writes the integer's four bytes in big-endian order ↗, which for −1 is FF FF FF FF in either order.
Try it¶
- Select four zero bytes and Subtract 1 as Unsigned Byte, then as Signed Short, then as Unsigned Int, reading the Inspector after each. The bytes agree every time and the numbers do not.
- Take a text file of mixed case and Subtract
0x20as Unsigned Byte. Search the result for bytes below0x20and count how many used to be spaces and digits. - Subtract 0 with Operand Step 1 over sixteen zero bytes. Write down the last byte before you look.
- Subtract
0x10from08 01as Unsigned Short under each Endian setting, and explain each result from the value the dialog read.
See also¶
- Add — the same wrap, from above
- Negate — zero minus the value, and the two values it leaves alone
- Binary Or — changing case by a bit instead of by arithmetic
- Bytes, hex and int — reading the same bytes signed and unsigned in Python