Skip to content

Bitwise Calculator — C# source

Perform AND, OR, XOR, NOT, shifts and rotates on 8/16/32/64-bit values with exact bigint math. Enter operands in binary, octal, decimal or hex and read the result in every base plus a live bit grid. Runs 100% in your browser.

This is the C# implementation — the same logic the interactive tool runs, in a shareable, citable form.

// =============================================================================
//  bitwise.cs — CosmoDev polyglot showcase port of the `bitwise` tool
//  -----------------------------------------------------------------------------
//  Language : C# 12 (.NET 8, standard library only)
//  Source:   ported from src/lib/bitwise.ts (the canonical, live TypeScript
//             lib); mirrors src/tool-sources/bitwise/{python.py,rust.rs}
//  License  : display source — part of CosmoDev's polyglot tool pages
//             (dev.cosmolabs.org). Shown verbatim alongside the JS/TS/Go/Rust/
//             Python ports and the other language ports.
//  -----------------------------------------------------------------------------
//  Pure, deterministic bitwise calculator. Zero deps. Operands are interpreted
//  as width-bit two's-complement values: any integer is normalized to the
//  half-open range [0, 2^width) before an operation, and every result is masked
//  back into that range — so the returned integer is always the unsigned
//  bit-pattern of the width-bit result.
//
//  C# note: the TS source uses arbitrary-precision `bigint`. .NET 8 ships
//  Int128/UInt128, so signed parsing uses Int128 and masked results use
//  UInt128 — mirroring the Rust port's i128/u128 approach. This is exact for
//  every supported width (8/16/32/64) and all shift intermediates. Only
//  literals beyond ~1.7e38 diverge from the TS lib (thrown as a clean
//  out-of-range), far outside any realistic bitwise-calculator input.
//
//  `BitwiseOps.Apply` is the port of the reference's `bitwise()` function (the
//  name is unavailable here — the containing class holds it).
// =============================================================================

namespace CosmoDev.Polyglot.Bitwise;

/// <summary>Numeric radix used for parsing and formatting.</summary>
public enum Base
{
    Bin,
    Oct,
    Dec,
    Hex,
}

/// <summary>Bit-width of the virtual register (8/16/32/64).</summary>
public enum Width
{
    W8 = 8,
    W16 = 16,
    W32 = 32,
    W64 = 64,
}

/// <summary>
/// Supported bitwise operation. <see cref="Not"/> is unary on
/// <c>a</c>; the rest are binary, with <c>b</c> as the shift/rotate count for
/// the shift/rotate ops.
/// </summary>
public enum Op
{
    And,
    Or,
    Xor,
    Not,
    Shl,
    Shr,
    Rol,
    Ror,
}

public static class BitwiseOps
{
    private static readonly string[] BaseNames = { "bin", "oct", "dec", "hex" };
    private static readonly int[] BaseRadixes = { 2, 8, 10, 16 };
    private static readonly string[] BaseDigits = { "01", "01234567", "0123456789", "0123456789abcdef" };

    /// <summary>Positional radix (2/8/10/16).</summary>
    private static int Radix(this Base b) => BaseRadixes[(int)b];

    /// <summary>Lowercase digit alphabet valid for this base.</summary>
    private static string Digits(this Base b) => BaseDigits[(int)b];

    /// <summary>Lowercase name, for error messages that match the TS lib.</summary>
    private static string Lower(this Base b) => BaseNames[(int)b];

    private static int WidthOf(this Width w) => (int)w;

    /// <summary>
    /// Parse a numeric string in <paramref name="b"/> into a signed integer.
    /// Strips 0x/0b/0o prefixes and an optional leading sign. Throws
    /// <see cref="ArgumentException"/> on empty input, any out-of-base digit,
    /// or a literal that overflows Int128. The raw signed value is returned
    /// (no width normalization); callers fold it into a field via
    /// <see cref="Normalize"/> / <see cref="Apply"/>.
    /// </summary>
    public static Int128 Parse(string value, Base b)
    {
        string trimmed = value.Trim();
        if (trimmed.Length == 0 || trimmed == "-")
            throw new ArgumentException($"Empty {b.Lower()} value", nameof(value));

        // Peel off an optional leading '-' so negative literals parse correctly.
        bool negative = trimmed[0] == '-';
        string body = negative ? trimmed[1..] : trimmed;

        // Strip each base prefix in turn (0x, then 0b, then 0o) from a lowered
        // copy — mirrors the reference's chained leading-prefix removal.
        // Matching against the lowercased copy keeps "0xFF" valid.
        string lowered = body.ToLowerInvariant();
        foreach (string prefix in new[] { "0x", "0b", "0o" })
            if (lowered.StartsWith(prefix, StringComparison.Ordinal))
                lowered = lowered[prefix.Length..];
        if (lowered.Length == 0)
            throw new ArgumentException($"Empty {b.Lower()} value", nameof(value));

        string allowed = b.Digits();
        int radix = b.Radix();
        Int128 acc = 0;
        foreach (char ch in lowered)
        {
            int digit = allowed.IndexOf(ch);
            if (digit < 0)
                throw new ArgumentException($"Invalid digit '{ch}' for base {b.Lower()}", nameof(value));
            // Checked arithmetic: an over-wide literal reports a clean error
            // instead of wrapping.
            try
            {
                acc = checked(acc * radix + digit);
            }
            catch (OverflowException)
            {
                throw new ArgumentException($"Value out of range for base {b.Lower()}", nameof(value));
            }
        }

        return negative ? -acc : acc;
    }

    /// <summary>Mask for a <paramref name="w"/>-bit field: 2^w - 1.</summary>
    public static UInt128 Mask(Width w) => (UInt128.One << w.WidthOf()) - 1;

    /// <summary>
    /// Normalize any signed value to its unsigned w-bit two's-complement
    /// value, i.e. into the half-open range [0, 2^w). The double-modulo form
    /// <c>((n % m) + m) % m</c> maps negative dividends into the canonical
    /// positive bucket (C#'s <c>%</c> is truncated, so the add + second mod
    /// is required) — e.g. -1 at width 8 yields 255.
    /// </summary>
    public static UInt128 Normalize(Int128 n, Width w)
    {
        Int128 m = (Int128)1 << w.WidthOf(); // 2^w
        return (UInt128)(((n % m) + m) % m);
    }

    /// <summary>
    /// Render <paramref name="n"/> in <paramref name="b"/>, zero-padded to at
    /// least <paramref name="minDigits"/> digits. Negatives carry a leading
    /// '-' and format their magnitude. (The third arg is a minimum digit
    /// count — the binary rendering of a width-bit value needs exactly
    /// <c>width</c> digits.)
    /// </summary>
    public static string Format(Int128 n, Base b, int minDigits)
    {
        if (n < 0)
        {
            // Format the magnitude and prepend the sign. The (-(n+1))+1 form
            // negates safely even at Int128.MinValue.
            UInt128 mag = (UInt128)(-(n + 1)) + 1;
            return "-" + FormatUnsigned(mag, b, minDigits);
        }
        return FormatUnsigned((UInt128)n, b, minDigits);
    }

    // Manual base conversion keeps parity with the sibling ports and yields
    // lowercase digits matching the TS `bigint.toString(radix)` output.
    private static string FormatUnsigned(UInt128 v, Base b, int minDigits)
    {
        const string alphabet = "0123456789abcdef";
        int radix = b.Radix();

        // Extract digits LSB-first; the do/while renders "0" for zero naturally.
        var buf = new List<char>();
        do
        {
            buf.Add(alphabet[(int)(v % (UInt128)(uint)radix)]);
            v /= (UInt128)(uint)radix;
        } while (v != 0);
        while (buf.Count < minDigits)
            buf.Insert(0, '0'); // zero-pad up to the minimum
        buf.Reverse();
        return new string(buf.ToArray());
    }

    /// <summary>
    /// Apply a w-bit operation. <paramref name="a"/> is the (unary) operand
    /// for <see cref="Op.Not"/>; <paramref name="b"/> is the second operand
    /// for binary ops and the shift/rotate count for shl/shr/rol/ror. Both
    /// operands are normalized to w-bit two's complement first; the result is
    /// masked to <paramref name="w"/> bits.
    /// </summary>
    /// <remarks>
    /// For shl/shr we short-circuit when the shift count meets or exceeds the
    /// width: every significant bit is shifted out, so the masked result is
    /// zero. This is the port of the reference's <c>bitwise()</c>.
    /// </remarks>
    public static UInt128 Apply(Op op, Int128 a, Int128 b, Width w)
    {
        UInt128 m = Mask(w);
        UInt128 x = Normalize(a, w);
        UInt128 y = Normalize(b, w);
        UInt128 width = (UInt128)w.WidthOf();

        switch (op)
        {
            case Op.And:
                return x & y;
            case Op.Or:
                return x | y;
            case Op.Xor:
                return x ^ y;
            case Op.Not:
                return ~x & m;
            case Op.Shl:
                // Left shift grows; for shift >= width the masked result is 0.
                return y >= width ? 0 : (x << (int)y) & m;
            case Op.Shr:
                // x is normalized non-negative → logical (zero-filling) shift.
                return y >= width ? 0 : x >> (int)y;
            case Op.Rol:
            case Op.Ror:
            {
                UInt128 shift = y % width; // rotate amount wraps within width
                if (shift == 0)
                    return x;
                // A right-rotate by `shift` is a left-rotate by (width - shift).
                UInt128 s = op == Op.Rol ? shift : width - shift;
                return ((x << (int)s) | (x >> (int)(width - s))) & m;
            }
            default:
                throw new ArgumentException($"Unknown bitwise operation: {op}");
        }
    }

    /// <summary>Fixed-width binary string of <paramref name="w"/> bits (MSB first).</summary>
    public static string ToBits(Int128 n, Width w) =>
        Format((Int128)Normalize(n, w), Base.Bin, w.WidthOf());

    /// <summary>
    /// Indices of set bits (LSB = index 0), normalized to
    /// <paramref name="w"/>. Stops at the highest set bit.
    /// </summary>
    public static List<int> Flags(Int128 n, Width w)
    {
        UInt128 v = Normalize(n, w);
        var outList = new List<int>();
        int i = 0;
        while (v != 0)
        {
            if ((v & 1) != 0)
                outList.Add(i);
            v >>= 1;
            i++;
        }
        return outList;
    }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →