<main>
  <div>
    <h2>Decimal to balanced-elevenary-in-Ogham</h2>
    <input type="number" id="decimal-in" value="17.38" step="any">
    <span id="digits-out"></span>
    <span id="roman-out"></span>
    <span id="ogham-out" class="ogham"></span>
  </div>
  <div>
    <h2>Balanced-elevenary-in-Ogham to decimal</h2>
    <input type="text" id="ogham-in" class="ogham" value="ᚆᚋᚂᚈ">
    <input type="text" id="roman-in">
    <span id="digits-in"></span>
    <span id="decimal-out"></span>
  </div>
</main>
main {
  display: flex;
  flex-flow: row;
  gap: 1em;
}

main > div {
  display: flex;
  flex-flow: column;
  flex: auto;
  width: 50%;
}

.ogham {
  font-size: 200%;
}
const decimalInEl = document.getElementById("decimal-in");
const digitsOutEl = document.getElementById("digits-out");
const romanOutEl = document.getElementById("roman-out");
const oghamOutEl = document.getElementById("ogham-out");
const oghamInEl = document.getElementById("ogham-in");
const romanInEl = document.getElementById("roman-in");
const digitsInEl = document.getElementById("digits-in");
const decimalOutEl = document.getElementById("decimal-out");

// "bit" and "trit" are cool and all but elevenary doesn't have anything
const FRAC_DIGITS = 5;

function intToBalancedElevenary(value: number) {
  const result = [];
  while (value != 0) {
    let lastDigit = value % 11;
    if (lastDigit > 5) {
      value += 11;
      lastDigit = lastDigit - 11;
    } else if (lastDigit < -5) {
      value -= 11;
      lastDigit = lastDigit + 11;
    }
    result.push(lastDigit);
    value = Math.trunc(value / 11);
  }
  result.reverse();
  if (result.length === 0) {
    return [0];
  }
  return result;
}

function fracToBalancedElevenary(value: number) {
  // This happens to be lazy as all hell.
  const offset = Math.pow(11, FRAC_DIGITS);
  const fracPart = Math.round(value * offset);
  let fracPartDigits = intToBalancedElevenary(fracPart);
  while (fracPartDigits.length < FRAC_DIGITS) {
    fracPartDigits = [0].concat(fracPartDigits);
  }
  while (fracPartDigits[fracPartDigits.length - 1] === 0) {
    fracPartDigits.pop();
  }
  return fracPartDigits;
}

const digitSpellings = {
  // M H D T C Q
  // ᚋ ᚆ ᚇ ᚈ ᚉ ᚊ
  "-5": { roman: "N", ogham: "ᚅ" },
  "-4": { roman: "S", ogham: "ᚄ" },
  "-3": { roman: "F", ogham: "ᚃ" },
  "-2": { roman: "L", ogham: "ᚂ" },
  "-1": { roman: "B", ogham: "ᚁ" },
  0: { roman: "M", ogham: "ᚋ" },
  1: { roman: "H", ogham: "ᚆ" },
  2: { roman: "D", ogham: "ᚇ" },
  3: { roman: "T", ogham: "ᚈ" },
  4: { roman: "C", ogham: "ᚉ" },
  5: { roman: "Q", ogham: "ᚊ" }
};

type DigitsResult = { intDigits: number[]; fracDigits: number[] };

function toBalancedElevenary(value: number): DigitsResult {
  const intDigits = intToBalancedElevenary(Math.trunc(value));
  const fracDigits = fracToBalancedElevenary(value - Math.trunc(value));
  return { intDigits, fracDigits };
}

function encodeRaw({ intDigits, fracDigits }: DigitsResult) {
  if (fracDigits.length > 0) {
    return intDigits.join(" : ") + " . " + fracDigits.join(" : ");
  } else {
    return intDigits.join(" : ");
  }
}

function encode(
  { intDigits, fracDigits }: DigitsResult,
  mode: "ogham" | "roman"
): string {
  const radix = mode === "ogham" ? "ᚖ" : ".";
  const intString = intDigits
    .map((digit) => digitSpellings[digit][mode])
    .join("");
  const fracString = fracDigits
    .map((digit) => digitSpellings[digit][mode])
    .join("");
  if (fracDigits.length > 0) {
    return intString + radix + fracString;
  } else {
    return intString;
  }
}

function renderDecimalInOghamOut() {
  const numericValue = parseFloat(decimalInEl.value);
  if (Number.isNaN(numericValue)) {
    digitsOutEl.innerText = romanOutEl.innerText = oghamOutEl.innerText =
      "error!";
    return;
  }
  const result = toBalancedElevenary(numericValue);
  digitsOutEl.innerText = encodeRaw(result);
  romanOutEl.innerText = encode(result, "roman");
  oghamOutEl.innerText = encode(result, "ogham");
}

renderDecimalInOghamOut();
decimalInEl.oninput = renderDecimalInOghamOut;

function decodeDigit(digit: string, mode: "ogham" | "roman"): number {
  for (let value = -5; value <= 5; value++) {
    if (digitSpellings[value][mode] === digit) {
      return value;
    }
  }
}

function decode(stringValue: string, mode: "ogham" | "roman"): DigitsResult {
  const radix = mode === "ogham" ? "ᚖ" : ".";
  const radixSplit = stringValue.split(radix);
  const intDigits = radixSplit[0]
    .split("")
    .map((digit) => decodeDigit(digit, mode));
  const fracDigits =
    radixSplit[1]?.split("").map((digit) => decodeDigit(digit, mode)) || [];
  return { intDigits, fracDigits };
}

function sum(data: number[]): number {
  return data.reduce((a, b) => a + b, 0);
}

function parse({ intDigits, fracDigits }: DigitsResult): number {
  return (
    sum(
      intDigits.map(
        (digitValue, digitIndex) =>
          digitValue * Math.pow(11, intDigits.length - digitIndex - 1)
      )
    ) +
    sum(
      fracDigits.map(
        (digitValue, digitIndex) => digitValue * Math.pow(11, -digitIndex - 1)
      )
    )
  );
}

function renderOghamInDecimalOut() {
  const decoded = decode(oghamInEl.value, "ogham");
  romanInEl.value = encode(decoded, "roman");
  digitsInEl.innerText = encodeRaw(decoded);
  decimalOutEl.innerText = parse(decoded);
}

function renderRomanInDecimalOut() {
  const decoded = decode(romanInEl.value, "roman");
  oghamInEl.value = encode(decoded, "ogham");
  digitsInEl.innerText = encodeRaw(decoded);
  decimalOutEl.innerText = parse(decoded);
}

renderOghamInDecimalOut();
oghamInEl.oninput = renderOghamInDecimalOut;
romanInEl.oninput = renderRomanInDecimalOut;
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.