From b57a40aad9a3c1c8af2278b99200f4a00b21a98a Mon Sep 17 00:00:00 2001
From: Rapha149 <49787110+Rapha149@users.noreply.github.com>
Date: Wed, 16 Oct 2024 20:43:26 +0200
Subject: [PATCH] Add tool floating-point-number-converter.
---
 components.d.ts                               |   1 +
 locales/de.yml                                |   4 +
 locales/en.yml                                |   4 +
 ...ating-point-number-converter.model.test.ts |  73 ++++++++
 .../floating-point-number-converter.model.ts  |  57 +++++++
 .../floating-point-number-converter.vue       | 159 ++++++++++++++++++
 .../floating-point-number-converter/index.ts  |  13 ++
 src/tools/index.ts                            |   2 +
 8 files changed, 313 insertions(+)
 create mode 100644 src/tools/floating-point-number-converter/floating-point-number-converter.model.test.ts
 create mode 100644 src/tools/floating-point-number-converter/floating-point-number-converter.model.ts
 create mode 100644 src/tools/floating-point-number-converter/floating-point-number-converter.vue
 create mode 100644 src/tools/floating-point-number-converter/index.ts
diff --git a/components.d.ts b/components.d.ts
index 3e65c3cc..1a18e59c 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -79,6 +79,7 @@ declare module '@vue/runtime-core' {
     Encryption: typeof import('./src/tools/encryption/encryption.vue')['default']
     EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
     FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
+    FloatingPointNumberConverter: typeof import('./src/tools/floating-point-number-converter/floating-point-number-converter.vue')['default']
     FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
     GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default']
     'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default']
diff --git a/locales/de.yml b/locales/de.yml
index e59ca114..469350d8 100644
--- a/locales/de.yml
+++ b/locales/de.yml
@@ -399,6 +399,10 @@ tools:
     description: >-
       Konvertiere Zahlen zwischen verschiedenen Basen (Dezimal, Hexadezimal,
       Binär, Oktal, Base64, ...).
+  floating-point-converter:
+    title: Konverter für Fließkommazahlen
+    description: >-
+      Konvertiere eine Dezimalzahl in eine IEEE-754 binäre Fließkommazahl oder umgekehrt.
   yaml-to-json-converter:
     title: YAML zu JSON
     description: Konvertiere YAML einfach in JSON mit diesem Live-Online-Konverter.
diff --git a/locales/en.yml b/locales/en.yml
index d1cd21c4..9779ba67 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -344,6 +344,10 @@ tools:
     title: Integer base converter
     description: Convert a number between different bases (decimal, hexadecimal, binary, octal, base64, ...)
 
+  floating-point-converter:
+    title: Floating point number converter
+    description: Convert a decimal number to a IEEE 754 binary floating point number or vice versa.
+
   yaml-to-json-converter:
     title: YAML to JSON converter
     description: Simply convert YAML to JSON with this online live converter.
diff --git a/src/tools/floating-point-number-converter/floating-point-number-converter.model.test.ts b/src/tools/floating-point-number-converter/floating-point-number-converter.model.test.ts
new file mode 100644
index 00000000..e956af03
--- /dev/null
+++ b/src/tools/floating-point-number-converter/floating-point-number-converter.model.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it } from 'vitest';
+import { convertBinaryToDecimal, convertDecimalToBinary } from './floating-point-number-converter.model';
+
+describe('floating-point-number-converter', () => {
+  describe('convertDecimalToBinary', () => {
+    it('Should convert a decimal number to a floating point binary number (IEEE-754)', () => {
+      // 32-Bit
+      expect(convertDecimalToBinary({ value: '0', bitCount: 32 })).toEqual('00000000000000000000000000000000');
+      expect(convertDecimalToBinary({ value: '-0', bitCount: 32 })).toEqual('10000000000000000000000000000000');
+      expect(convertDecimalToBinary({ value: 'NaN', bitCount: 32 })).toEqual('01111111110000000000000000000000');
+      expect(convertDecimalToBinary({ value: 'Infinity', bitCount: 32 })).toEqual('01111111100000000000000000000000');
+      expect(convertDecimalToBinary({ value: '-Infinity', bitCount: 32 })).toEqual('11111111100000000000000000000000');
+      expect(convertDecimalToBinary({ value: '2.5', bitCount: 32 })).toEqual('01000000001000000000000000000000');
+      expect(convertDecimalToBinary({ value: '-128.25', bitCount: 32 })).toEqual('11000011000000000100000000000000');
+      expect(convertDecimalToBinary({ value: '0.1', bitCount: 32 })).toEqual('00111101110011001100110011001101');
+      expect(convertDecimalToBinary({ value: '3.4028235e38', bitCount: 32 })).toEqual('01111111011111111111111111111111');
+      expect(convertDecimalToBinary({ value: '1.1754942e-38', bitCount: 32 })).toEqual('00000000011111111111111111111111');
+
+      // 64-Bit
+      expect(convertDecimalToBinary({ value: '0', bitCount: 64 })).toEqual('0000000000000000000000000000000000000000000000000000000000000000');
+      expect(convertDecimalToBinary({ value: '-0', bitCount: 64 })).toEqual('1000000000000000000000000000000000000000000000000000000000000000');
+      expect(convertDecimalToBinary({ value: 'NaN', bitCount: 64 })).toEqual('0111111111111000000000000000000000000000000000000000000000000000');
+      expect(convertDecimalToBinary({ value: 'Infinity', bitCount: 64 })).toEqual('0111111111110000000000000000000000000000000000000000000000000000');
+      expect(convertDecimalToBinary({ value: '-Infinity', bitCount: 64 })).toEqual('1111111111110000000000000000000000000000000000000000000000000000');
+      expect(convertDecimalToBinary({ value: '2.5', bitCount: 64 })).toEqual('0100000000000100000000000000000000000000000000000000000000000000');
+      expect(convertDecimalToBinary({ value: '-128.25', bitCount: 64 })).toEqual('1100000001100000000010000000000000000000000000000000000000000000');
+      expect(convertDecimalToBinary({ value: '0.1', bitCount: 64 })).toEqual('0011111110111001100110011001100110011001100110011001100110011010');
+      expect(convertDecimalToBinary({ value: '1.7976931348623157e308', bitCount: 64 })).toEqual('0111111111101111111111111111111111111111111111111111111111111111');
+      expect(convertDecimalToBinary({ value: '2.225073858507201e-308', bitCount: 64 })).toEqual('0000000000001111111111111111111111111111111111111111111111111111');
+    });
+  });
+  describe('convertBinaryToDecimal', () => {
+    it('Should convert a floating point binary number (IEEE-754) to a decimal number', () => {
+      // 32-Bit
+      expect(convertBinaryToDecimal({ value: '00000000000000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('0.00000000000000000000000000000000');
+      expect(convertBinaryToDecimal({ value: '10000000000000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('-0.00000000000000000000000000000000');
+      expect(convertBinaryToDecimal({ value: '01111111110000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('NaN');
+      expect(convertBinaryToDecimal({ value: '11111111110000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('NaN');
+      expect(convertBinaryToDecimal({ value: '01111111110000000000000000000001', decimalPrecision: '32', removeZeroPadding: false })).toEqual('NaN');
+      expect(convertBinaryToDecimal({ value: '01111111100000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('Infinity');
+      expect(convertBinaryToDecimal({ value: '11111111100000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('-Infinity');
+      expect(convertBinaryToDecimal({ value: '01000000001000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('2.50000000000000000000000000000000');
+      expect(convertBinaryToDecimal({ value: '01000000001000000000000000000000', decimalPrecision: '32', removeZeroPadding: true })).toEqual('2.5');
+      expect(convertBinaryToDecimal({ value: '11000011000000000100000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('-128.25000000000000000000000000000000');
+      expect(convertBinaryToDecimal({ value: '00111101110011001100110011001101', decimalPrecision: '32', removeZeroPadding: false })).toEqual('0.10000000149011611938476562500000');
+      expect(convertBinaryToDecimal({ value: '00111101110011001100110011001101', decimalPrecision: '10', removeZeroPadding: false })).toEqual('0.1000000015');
+      expect(convertBinaryToDecimal({ value: '00111101110011001100110011001101', decimalPrecision: '1', removeZeroPadding: false })).toEqual('0.1');
+      expect(convertBinaryToDecimal({ value: '01111111011111111111111111111111', decimalPrecision: '32', removeZeroPadding: false })).matches(/^3\.402823\d*e\+?38$/);
+      expect(convertBinaryToDecimal({ value: '00000000011111111111111111111111', decimalPrecision: '32', removeZeroPadding: false })).toEqual('0.00000000000000000000000000000000');
+      expect(convertBinaryToDecimal({ value: '00000000011111111111111111111111', decimalPrecision: '64', removeZeroPadding: false })).toEqual('0.0000000000000000000000000000000000000117549421069244107548702944');
+      expect(convertBinaryToDecimal({ value: '00000000011111111111111111111111', decimalPrecision: '', removeZeroPadding: false })).matches(/^1\.1754942\d*e-38$/);
+
+      // 64-Bit
+      expect(convertBinaryToDecimal({ value: '0000000000000000000000000000000000000000000000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('0.00000000000000000000000000000000');
+      expect(convertBinaryToDecimal({ value: '1000000000000000000000000000000000000000000000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('-0.00000000000000000000000000000000');
+      expect(convertBinaryToDecimal({ value: '0111111111111000000000000000000000000000000000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('NaN');
+      expect(convertBinaryToDecimal({ value: '1111111111111000000000000000000000000000000000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('NaN');
+      expect(convertBinaryToDecimal({ value: '0111111111111000000000000000000000000000000000000000000000000001', decimalPrecision: '32', removeZeroPadding: false })).toEqual('NaN');
+      expect(convertBinaryToDecimal({ value: '0111111111110000000000000000000000000000000000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('Infinity');
+      expect(convertBinaryToDecimal({ value: '1111111111110000000000000000000000000000000000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('-Infinity');
+      expect(convertBinaryToDecimal({ value: '0100000000000100000000000000000000000000000000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('2.50000000000000000000000000000000');
+      expect(convertBinaryToDecimal({ value: '0100000000000100000000000000000000000000000000000000000000000000', decimalPrecision: '32', removeZeroPadding: true })).toEqual('2.5');
+      expect(convertBinaryToDecimal({ value: '1100000001100000000010000000000000000000000000000000000000000000', decimalPrecision: '32', removeZeroPadding: false })).toEqual('-128.25000000000000000000000000000000');
+      expect(convertBinaryToDecimal({ value: '0011111110111001100110011001100110011001100110011001100110011010', decimalPrecision: '32', removeZeroPadding: false })).toEqual('0.10000000000000000555111512312578');
+      expect(convertBinaryToDecimal({ value: '0011111110111001100110011001100110011001100110011001100110011010', decimalPrecision: '10', removeZeroPadding: false })).toEqual('0.1000000000');
+      expect(convertBinaryToDecimal({ value: '0011111110111001100110011001100110011001100110011001100110011010', decimalPrecision: '1', removeZeroPadding: false })).toEqual('0.1');
+      expect(convertBinaryToDecimal({ value: '0111111111101111111111111111111111111111111111111111111111111111', decimalPrecision: '32', removeZeroPadding: false })).matches(/^1\.79769313\d*e\+?308$/);
+      expect(convertBinaryToDecimal({ value: '0000000000001111111111111111111111111111111111111111111111111111', decimalPrecision: '32', removeZeroPadding: false })).toEqual('0.00000000000000000000000000000000');
+      expect(convertBinaryToDecimal({ value: '0000000000001111111111111111111111111111111111111111111111111111', decimalPrecision: '100', removeZeroPadding: false })).toEqual('0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000');
+      expect(convertBinaryToDecimal({ value: '0000000000001111111111111111111111111111111111111111111111111111', decimalPrecision: '', removeZeroPadding: false })).matches(/^2\.2250738585\d*e-308$/);
+    });
+  });
+});
diff --git a/src/tools/floating-point-number-converter/floating-point-number-converter.model.ts b/src/tools/floating-point-number-converter/floating-point-number-converter.model.ts
new file mode 100644
index 00000000..7cdd80d5
--- /dev/null
+++ b/src/tools/floating-point-number-converter/floating-point-number-converter.model.ts
@@ -0,0 +1,57 @@
+export function convertDecimalToBinary({ value, bitCount }: { value: string; bitCount: number }) {
+  let number = Number.parseFloat(value.replace(/\s/g, ''));
+  if (value.match(/^-?inf(inity)?$/i)) {
+    // const sign = value.startsWith('-') ? 1 : 0;
+    // return `${sign}${'1'.repeat(exponentBits)}${'0'.repeat(mantissaBits)}`;
+    number = (value.startsWith('-') ? -2 : 2) / 0;
+  }
+
+  switch (bitCount) {
+    case 32: { // Single precision
+      const uint = new Uint32Array(Float32Array.of(number).buffer);
+      return uint[0].toString(2).padStart(32, '0');
+    }
+    case 64: { // Double precision
+      const uint = new Uint32Array(Float64Array.of(number).buffer);
+      return [...uint].slice(0, 2).reverse().map(p => p.toString(2).padStart(32, '0')).join('');
+    }
+    default:
+      throw new Error('Unsupported bit count. Only 32 and 64 are allowed.');
+  }
+}
+
+export function convertBinaryToDecimal({ value, decimalPrecision, removeZeroPadding }: { value: string; decimalPrecision: string; removeZeroPadding: boolean }) {
+  if (value.match(/[^01]/)) {
+    throw new Error('Not a binary number.');
+  }
+  if (decimalPrecision.match(/[^\d]/)) {
+    throw new Error('Decimal Precision must be a positive whole number.');
+  }
+
+  let result: number;
+  switch (value.length) {
+    case 32: {
+      const binary = [Number.parseInt(value, 2)];
+      result = (new Float32Array(Uint32Array.from(binary).buffer))[0];
+      break;
+    }
+    case 64: {
+      const binary = [Number.parseInt(value.substring(32), 2), Number.parseInt(value.substring(0, 32), 2)];
+      result = (new Float64Array(Uint32Array.from(binary).buffer))[0];
+      break;
+    }
+    default:
+      throw new Error('Invalid length. Supply a binary string with length 32 or 64.');
+  }
+
+  const zeroNegative = result === 0 && 2 / result === Number.NEGATIVE_INFINITY;
+  let resultString = decimalPrecision.length === 0 ? result.toString() : result.toFixed(Number.parseInt(decimalPrecision));
+  if (removeZeroPadding) {
+    resultString = resultString.replace(/\.(\d+?)(0+)$/, '.$1');
+  }
+  return (zeroNegative ? '-' : '') + resultString;
+}
+
+export function calcErrorDueToConversion({ decimalInput, actualValue }: { decimalInput: string; actualValue: string }) {
+  return (Number.parseFloat(decimalInput) - Number.parseFloat(actualValue)).toFixed(32).replace(/\.(\d+?)(0+)$/, '.$1');
+}
diff --git a/src/tools/floating-point-number-converter/floating-point-number-converter.vue b/src/tools/floating-point-number-converter/floating-point-number-converter.vue
new file mode 100644
index 00000000..a32fc52d
--- /dev/null
+++ b/src/tools/floating-point-number-converter/floating-point-number-converter.vue
@@ -0,0 +1,159 @@
+
+
+
+  
+    
+
+    
+
+    
+
+    
+
+    
+
+    
+
+    
+  
+
+  
+    
+
+    
+
+    
+      
+    
+
+    
+      {{ binaryToDecimalError }}
+    
+
+    
+
+    
+  
+
diff --git a/src/tools/floating-point-number-converter/index.ts b/src/tools/floating-point-number-converter/index.ts
new file mode 100644
index 00000000..aac38164
--- /dev/null
+++ b/src/tools/floating-point-number-converter/index.ts
@@ -0,0 +1,13 @@
+import { ArrowsLeftRight } from '@vicons/tabler';
+import { defineTool } from '../tool';
+import { translate } from '@/plugins/i18n.plugin';
+
+export const tool = defineTool({
+  name: translate('tools.floating-point-converter.title'),
+  path: '/floating-point-converter',
+  description: translate('tools.floating-point-converter.description'),
+  keywords: ['converter', 'floating', 'point', 'number', 'converter', 'binary', 'decimal'],
+  component: () => import('./floating-point-number-converter.vue'),
+  icon: ArrowsLeftRight,
+  createdAt: new Date('2024-10-12'),
+});
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 388cfaf4..8025b8a3 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -1,6 +1,7 @@
 import { tool as base64FileConverter } from './base64-file-converter';
 import { tool as base64StringConverter } from './base64-string-converter';
 import { tool as basicAuthGenerator } from './basic-auth-generator';
+import { tool as floatingPointNumberConverter } from './floating-point-number-converter';
 import { tool as emailNormalizer } from './email-normalizer';
 
 import { tool as asciiTextDrawer } from './ascii-text-drawer';
@@ -98,6 +99,7 @@ export const toolsByCategory: ToolCategory[] = [
     components: [
       dateTimeConverter,
       baseConverter,
+      floatingPointNumberConverter,
       romanNumeralConverter,
       base64StringConverter,
       base64FileConverter,