When I first wrote a signed-binary converter, the obvious implementation was “call toString(2) and let JavaScript do the rest.” That works for positive integers, but it hides the two decisions a useful converter must expose: whether a value fits a chosen width, and which interpretation of the leading bit the reader wants. The Be Good Tool converter keeps those steps explicit.
A bit width changes the valid range
For N signed bits, the range is -2^(N-1) through 2^(N-1)-1. The source checks that before formatting:
function limitNum(num, numLen) {
const baseNum = 2 ** (numLen - 1);
const maxNum = baseNum - 1;
const minNum = 0 - baseNum;
let checkResult = num > maxNum || num < minNum ? 0 : 1;
const maxNum2 = baseNum;
const minNum2 = 0 - baseNum;
if (checkResult === 0 && num <= maxNum2 && num >= minNum2) {
checkResult = 2;
}
return checkResult;
}
The second check distinguishes a value outside the signed range but still inside the unsigned boundary. That gives the UI enough information to explain the failure instead of silently wrapping. For eight bits, 127 is valid signed input, 128 is not signed input, and -128 is valid signed input. Treating those as a symmetric abs(value) check would be wrong.
Padding is part of the representation
The converter first uses the magnitude:
function toBinary(num) {
return Math.abs(num).toString(2);
}
function fillBitWith_0_Signed(numBinary, bitLen) {
return numBinary.padStart(bitLen, "0");
}
Padding is not cosmetic. Without it, 5 could be displayed as 101, but the eight-bit complement of the same number needs 00000101. The selected width determines which bit becomes the sign bit and therefore changes the meaning of every later operation.
One’s complement and two’s complement are string algorithms
The source keeps the bits as strings so leading zeroes survive:
function notString(numBinary) {
const out = [];
for (let i = 0; i < numBinary.length; i++) {
parseInt(numBinary[i]) ? out.push("0") : out.push("1");
}
return out.join("");
}
function one2two(numFullBinary) {
const bits = numFullBinary.split("");
for (let i = numFullBinary.length; i > 0; i--) {
if (parseInt(numFullBinary[i - 1])) bits[i - 1] = "0";
else {
bits[i - 1] = "1";
return bits.join("");
}
}
return numFullBinary;
}
notString flips each bit for one’s complement. one2two walks from the right, turning trailing ones into zeroes until it finds a zero to turn into one. It is the same carry propagation a hardware adder performs, but visible in JavaScript. Returning the original all-ones string documents the fixed-width overflow case instead of expanding the result.
Converting back requires choosing an interpretation
For a two’s-complement bit string, the first bit has negative weight:
function bin2Dec_2(para) {
const bits = (para || inputNumber.value + "").toString().split("");
let value = bits[0] * -(2 ** (bits.length - 1));
for (let i = bits.length - 2; i >= 0; i--) {
value += bits[bits.length - 1 - i] * 2 ** i;
}
return value;
}
The UI also reports ordinary parseInt(numBin, 2), one’s-complement decimal, and the recovered source patterns. That is useful when debugging a protocol or a fixed-width serialization, because the same characters can represent different numbers under unsigned, one’s-complement, and two’s-complement rules.
The limitation is deliberate: input is restricted to safe JavaScript integers and a positive bit length. Extremely large widths can make 2 ** (N - 1) lose numeric precision, and a binary string is still validated as characters rather than a BigInt. This is a teaching and inspection tool, not an arbitrary-precision binary library.
The awkward zero and overflow cases matter
Fixed-width representations contain edge cases that are easy to hide in a polished result. The source treats "-0" specially while producing complement output, because one’s complement has both positive and negative zero patterns while two’s complement does not. That is exactly the kind of detail a generic integer conversion API will erase.
There is another useful distinction in the reverse direction. bin2Dec_res first validates that every character is either "0" or "1", then reports an unsigned parseInt result. Only when the leading bit is one does it add the signed one’s- and two’s-complement interpretations. This ordering lets a reader compare representations without claiming that one interpretation is universally correct.
The converter also groups decimal-looking output with spaces using format, but that formatting is presentation only. It must never be fed back into the binary validation path because spaces are not bits. Separating the displayed grouping from the stored string is a small rule that prevents copy-and-paste bugs.
If I extended this implementation, I would use BigInt or a dedicated bit-string arithmetic layer for widths beyond safe-number precision. I would also make signed versus unsigned mode a first-class label rather than relying on the result rows. The current version is intentionally compact, but its explicit range check is already a good reminder: representation is part of the input, not an afterthought.
Why keeping both directions in one tool helps
The forward and reverse operations also make a useful sanity check for each other. I can enter a decimal value, choose a width, copy the displayed bit pattern, and send that pattern through the binary-to-decimal path. For a valid signed pattern, the two’s-complement interpretation should return the expected value, while the ordinary unsigned interpretation may deliberately return something else. Seeing both numbers makes the sign bit concrete instead of leaving it as a rule to memorize.
The width must remain attached to that experiment. 00000101 and 101 contain the same visible one bits, but they do not describe the same fixed-width signed representation. In the shorter string, the first bit is one and therefore carries negative weight under the signed two’s-complement function. This is why the converter pads before calculating complements and why trimming leading zeroes would destroy useful information.
I also prefer the explicit loops here over relying on JavaScript bitwise operators. JavaScript bitwise operators coerce values to signed 32-bit integers, which would make an apparently arbitrary bit-length input quietly behave like a 32-bit operation. The source’s string operations avoid that particular coercion and make the selected width visible in every result. They do not solve arbitrary precision, but they preserve the educational behavior the interface promises.
I turned this implementation into a small free tool: Two’s Complement Calculator.
Top comments (0)