Skip to content
Snippets Groups Projects
Commit eb951caa authored by Benedikt's avatar Benedikt
Browse files

Added JavaScript Implementation

parent 0e23ebdd
No related branches found
No related tags found
No related merge requests found
Pipeline #51701 failed
...@@ -21,3 +21,5 @@ env/ ...@@ -21,3 +21,5 @@ env/
venv venv
.vscode/ .vscode/
dsiUnits-js/node_modules/
{
"presets": [
["@babel/preset-env", { "targets": { "node": "current" } }]
]
}
This diff is collapsed.
{
"name": "dsiunits",
"version": "1.0.0",
"type": "module",
"scripts": {
"test": "jest"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-jest": "^27.0.0",
"jest": "^27.0.0"
}
}
// dsiParser.js
import { DSIUnitNode } from './dsiUnitNode.js';
import { dsiPrefixesHTML, dsiUnitsHTML, dsiKeyWords } from './unitStrings.js';
class DSIParser {
constructor() {
// Default options
this.createPerByDivision = true;
this.maxDenominator = 10000;
// Default wrappers for HTML output are not needed since we output directly HTML
}
// Singleton pattern
static getInstance() {
if (!DSIParser.instance) {
DSIParser.instance = new DSIParser();
}
return DSIParser.instance;
}
parse(dsiString) {
const warnings = [];
// Check for non-DSI marker
if (dsiString.length > 0 && dsiString[0] === '|') {
warnings.push("Parsing a correctly marked non D-SI unit!");
return {
original: dsiString,
tree: [[new DSIUnitNode('', dsiString.slice(1), '', false)]],
warnings,
nonDsiUnit: true
};
}
// Replace double backslashes with single backslash
while (dsiString.includes('\\\\')) {
warnings.push(`Double backslash found in string, treating as one backslash: «${dsiString}»`);
dsiString = dsiString.replace(/\\\\/g, '\\');
}
if (dsiString === "") {
warnings.push("Given D-SI string is empty!");
return {
original: 'NULL',
tree: [[new DSIUnitNode('', 'NULL', '', false)]],
warnings,
nonDsiUnit: true
};
}
// Parse fractions: split at "\per" (literal string "\per")
let modifiedStr = dsiString.replace('percent', 'prozent');
let parts = modifiedStr.split('\\per').map(s => s.replace('prozent', 'percent'));
// Check for empty fraction parts
parts = parts.filter(part => {
if (part.length === 0) {
warnings.push(`The dsi string contains a \\per missing a numerator or denominator! Given string: ${dsiString}`);
return false;
}
return true;
});
if (parts.length > 2) {
warnings.push(`The dsi string contains more than one \\per, does not match specs! Given string: ${dsiString}`);
}
// For each fraction, parse the fractionless part.
const tree = parts.map(part => {
return this._parseFractionlessDsi(part, warnings);
});
return {
original: dsiString,
tree,
warnings,
nonDsiUnit: false
};
}
_parseFractionlessDsi(dsiString, warnings) {
let items = dsiString.split('\\');
if (items[0] === '') {
items.shift();
} else {
warnings.push(`String should start with a backslash, string given was «${dsiString}»`);
}
const nodes = [];
while (items.length) {
let prefix = '';
let unit = '';
let exponent = '';
let valid = true;
if (items.length && items[0] in dsiPrefixesHTML) {
prefix = items.shift();
}
if (items.length) {
unit = items.shift();
} else {
warnings.push(`This D-SI unit seems to be missing the base unit! \\${prefix}`);
valid = false;
}
if (items.length && /^tothe\{[^{}]*\}$/.test(items[0])) {
const expToken = items.shift();
const match = expToken.match(/tothe\{([^{}]+)\}/);
if (match) {
exponent = match[1];
if (isNaN(Number(exponent))) {
warnings.push(`The exponent «${exponent}» is not a number!`);
valid = false;
}
}
}
// If a unit token was provided but is not valid, warn and mark invalid.
if (unit === '') {
warnings.push(`This D-SI unit seems to be missing the base unit! \\${prefix}${exponent ? '\\tothe{' + exponent + '}' : ''}`);
valid = false;
} else if (!(unit in dsiUnitsHTML)) {
warnings.push(`The identifier «${unit}» does not match any D-SI units!`);
valid = false;
}
nodes.push(new DSIUnitNode(prefix, unit, exponent, valid));
}
return nodes;
}
}
export default DSIParser;
// dsiUnit.js
import DSIParser from './dsiParser.js';
import { DSIUnitNode } from './dsiUnitNode.js';
export class DSIUnit {
constructor(dsiString) {
// Use the parser to parse the string.
const parser = DSIParser.getInstance();
const result = parser.parse(dsiString);
this.dsiString = result.original;
this.tree = result.tree; // tree is an array of fractions (each fraction is an array of DSIUnitNode)
this.warnings = result.warnings;
this.nonDsiUnit = result.nonDsiUnit;
this.valid = (this.warnings.length === 0);
// Stub: scaleFactor will remain 1.0
this.scaleFactor = 1.0;
}
/**
* Generates HTML output representing the D-SI unit.
* For fractions, output a fraction structure.
*/
toHTML({ wrapper = '', prefix = '', suffix = '' } = {}) {
// If non-DSI unit, simply wrap it in a special span.
if (this.nonDsiUnit) {
const content = `<span class="dsi-nonunit">${this.dsiString.startsWith('|') ? this.dsiString.slice(1) : this.dsiString}</span>`;
return `${wrapper}${prefix}${content}${suffix}${wrapper}`;
}
let htmlOutput = '';
if (this.tree.length === 1) {
// No fraction: simply join the nodes with a small separator.
htmlOutput = this.tree[0].map(node => node.toHTML()).join('<span class="dsi-mul">&nbsp;</span>');
} else if (this.tree.length === 2) {
// Fraction: generate numerator and denominator.
const numerator = this.tree[0].map(node => node.toHTML()).join('<span class="dsi-mul">&nbsp;</span>');
const denominator = this.tree[1].map(node => node.toHTML()).join('<span class="dsi-mul">&nbsp;</span>');
htmlOutput = `<span class="dsi-fraction"><span class="dsi-numerator">${numerator}</span><span class="dsi-denom">${denominator}</span></span>`;
} else {
// More than two fractions, join with a red slash as a warning.
htmlOutput = this.tree.map((frac, idx) => {
const part = frac.map(node => node.toHTML()).join('<span class="dsi-mul">&nbsp;</span>');
return part;
}).join('<span class="dsi-invalid">/</span>');
}
// If scaleFactor is not 1.0, prepend it.
if (this.scaleFactor !== 1.0) {
htmlOutput = `<span class="dsi-scale">${this.scaleFactor}&middot;</span>` + htmlOutput;
}
return `${wrapper}${prefix}${htmlOutput}${suffix}${wrapper}`;
}
// Stub methods for math operations:
getScaleFactor(other) {
// Stub: not implemented
return NaN;
}
getBaseUnit(other) {
// Stub: not implemented
return null;
}
toString() {
// A simple text representation that joins all nodes
if (this.nonDsiUnit) return '|' + this.dsiString;
return this.tree.map(frac => frac.map(node => node.toString()).join('')).join('\\per');
}
}
// dsiUnitNode.js
import { dsiPrefixesHTML, dsiUnitsHTML } from './unitStrings.js';
export class DSIUnitNode {
constructor(prefix, unit, exponent = '', valid = true, scaleFactor = 1.0) {
this.prefix = prefix;
this.unit = unit;
this.valid = valid;
// Normalize exponent: if empty or "1", treat as 1.
if (exponent === '' || exponent === '1') {
this.exponent = 1;
} else {
const numExp = Number(exponent);
this.exponent = isNaN(numExp) ? exponent : numExp;
}
this.scaleFactor = scaleFactor;
}
toHTML() {
let parts = [];
if (this.prefix) {
if (this.prefix in dsiPrefixesHTML) {
parts.push(`<span class="dsi-unit">${dsiPrefixesHTML[this.prefix]}</span>`);
} else {
parts.push(`<span class="dsi-unit"><span class="dsi-invalid">${this.prefix}</span></span>`);
}
}
if (this.unit) {
if (this.unit in dsiUnitsHTML) {
parts.push(`<span class="dsi-unit">${dsiUnitsHTML[this.unit]}</span>`);
} else {
parts.push(`<span class="dsi-unit"><span class="dsi-invalid">${this.unit}</span></span>`);
}
}
let baseHTML = parts.join('');
if (this.exponent !== 1) {
if (this.exponent === 0.5) {
return `<span class="dsi-sqrt">√(${baseHTML})</span>`;
} else {
return `${baseHTML}<sup class="dsi-exponent">${this.exponent}</sup>`;
}
}
return baseHTML;
}
toString() {
let str = '';
if (this.prefix) str += '\\' + this.prefix;
str += '\\' + this.unit;
if (this.exponent !== 1) str += `\\tothe{${this.exponent}}`;
return str;
}
}
// unitStrings.js
export const dsiPrefixesHTML = {
deca: 'da',
hecto: 'h',
kilo: 'k',
mega: 'M',
giga: 'G',
tera: 'T',
peta: 'P',
exa: 'E',
zetta: 'Z',
yotta: 'Y',
deci: 'd',
centi: 'c',
milli: 'm',
micro: 'µ',
nano: 'n',
pico: 'p',
femto: 'f',
atto: 'a',
zepto: 'z',
yocto: 'y',
kibi: 'Ki',
mebi: 'Mi',
gibi: 'Gi',
tebi: 'Ti',
pebi: 'Pi',
exbi: 'Ei',
zebi: 'Zi',
yobi: 'Yi'
};
export const dsiUnitsHTML = {
metre: 'm',
kilogram: 'kg',
second: 's',
ampere: 'A',
kelvin: 'K',
mole: 'mol',
candela: 'cd',
one: '1',
day: 'd',
hour: 'h',
minute: 'min',
degree: '°',
arcminute: '',
arcsecond: '',
gram: 'g',
radian: 'rad',
steradian: 'sr',
hertz: 'Hz',
newton: 'N',
pascal: 'Pa',
joule: 'J',
watt: 'W',
coulomb: 'C',
volt: 'V',
farad: 'F',
ohm: 'Ω',
siemens: 'S',
weber: 'Wb',
tesla: 'T',
henry: 'H',
degreecelsius: '°C',
lumen: 'lm',
lux: 'lx',
becquerel: 'Bq',
sievert: 'Sv',
gray: 'Gy',
katal: 'kat',
hectare: 'ha',
litre: 'l',
tonne: 't',
electronvolt: 'eV',
dalton: 'Da',
astronomicalunit: 'au',
neper: 'Np',
bel: 'B',
decibel: 'dB',
percent: '%',
ppm: 'ppm',
byte: 'Byte',
bit: 'bit',
angstrom: 'Å',
bar: 'bar',
// Extend as needed...
};
export const dsiKeyWords = {
tothe: '\\tothe',
per: '\\per'
};
// dsiUnits.test.js
import { DSIUnit } from '../src/dsiUnit.js';
import { DSIUnitNode } from '../src/dsiUnitNode.js';
describe('DSIUnit HTML Output Tests', () => {
test('Base case: one unit without prefix or exponent', () => {
const unit = new DSIUnit('\\metre');
// We expect a single node with unit "metre" and exponent 1
expect(unit.tree.length).toBe(1);
expect(unit.tree[0].length).toBe(1);
const node = unit.tree[0][0];
expect(node.unit).toBe('metre');
expect(node.exponent).toBe(1);
// Check HTML output
expect(unit.toHTML()).toBe('<span class="dsi-unit">m</span>');
expect(unit.valid).toBe(true);
expect(unit.warnings).toEqual([]);
});
test('Exponent handling: metre^2 and square-root', () => {
const unit2 = new DSIUnit('\\metre\\tothe{2}');
expect(unit2.toHTML()).toBe('<span class="dsi-unit">m</span><sup class="dsi-exponent">2</sup>');
const unitSqrt = new DSIUnit('\\metre\\tothe{0.5}');
expect(unitSqrt.toHTML()).toBe('<span class="dsi-sqrt">√(<span class="dsi-unit">m</span>)</span>');
});
test('Fraction: e.g. mega metre per second^2', () => {
const unitFrac = new DSIUnit('\\mega\\metre\\per\\second\\tothe{2}');
// Expected: numerator: mega metre; denominator: second^2
const expectedHTML = `<span class="dsi-fraction"><span class="dsi-numerator"><span class="dsi-unit">M</span><span class="dsi-unit">m</span></span><span class="dsi-denom"><span class="dsi-unit">s</span><sup class="dsi-exponent">2</sup></span></span>`;
expect(unitFrac.toHTML()).toBe(expectedHTML);
});
test('Robustness: unknown unit', () => {
const unitUnknown = new DSIUnit('\\foo');
const expectedHTML = `<span class="dsi-unit"><span class="dsi-invalid">foo</span></span>`;
expect(unitUnknown.toHTML()).toBe(expectedHTML);
expect(unitUnknown.valid).toBe(false);
expect(unitUnknown.warnings).toContain('The identifier «foo» does not match any D-SI units!');
});
test('Non D-SI unit marker', () => {
const nonDsi = new DSIUnit('|NonDsiUnit');
const expectedHTML = `<span class="dsi-nonunit">NonDsiUnit</span>`;
expect(nonDsi.toHTML()).toBe(expectedHTML);
});
test('Empty string', () => {
const emptyUnit = new DSIUnit('');
const expectedHTML = `<span class="dsi-nonunit">NULL</span>`;
expect(emptyUnit.toHTML()).toBe(expectedHTML);
expect(emptyUnit.warnings).toContain('Given D-SI string is empty!');
});
test('Double backslash handling', () => {
const unitDouble = new DSIUnit('\\\\metre\\per\\second');
// Should be treated as single backslashes.
const expectedHTML = `<span class="dsi-fraction"><span class="dsi-numerator"><span class="dsi-unit">m</span></span><span class="dsi-denom"><span class="dsi-unit">s</span></span></span>`;
expect(unitDouble.toHTML()).toBe(expectedHTML);
expect(unitDouble.warnings.some(w => w.includes('Double backslash'))).toBe(true);
});
});
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Title</title> <title>D-SI Units Visual Test</title>
<style>
body {
font-family: sans-serif;
margin: 20px;
}
table {
border-collapse: collapse;
width: 100%;
margin-bottom: 40px;
}
th, td {
border: 1px solid #ccc;
padding: 8px;
text-align: left;
vertical-align: top;
}
pre {
margin: 0;
font-family: monospace;
background: #f8f8f8;
padding: 4px;
}
.dsi-invalid {
color: red;
font-weight: bold;
}
.dsi-sqrt {
font-style: italic;
}
</style>
</head> </head>
<body> <body>
<h1>D-SI Units Visual Test</h1>
<h2>Valid D-SI Strings</h2>
<table id="valid-table">
<thead>
<tr>
<th>Raw D-SI String</th>
<th>Rendered Output</th>
</tr>
</thead>
<tbody></tbody>
</table>
<h2>Invalid D-SI Strings</h2>
<table id="invalid-table">
<thead>
<tr>
<th>Raw D-SI String</th>
<th>Rendered Output</th>
<th>Warnings</th>
</tr>
</thead>
<tbody></tbody>
</table>
<script type="module">
// Import your DSIUnit class from the src directory.
import { DSIUnit } from "../src/dsiUnit.js";
// Define arrays of sample D-SI strings.
// Valid examples now include ones using \per as well as fractional exponents tothe{0.333333} and tothe{0.25}.
const validDSI = [
'\\metre',
'\\metre\\tothe{2}',
'\\metre\\tothe{0.5}',
'\\mega\\metre\\per\\second\\tothe{2}',
'\\kilo\\metre\\per\\hour',
'\\metre\\per\\second', // simple fraction: metre per second
'\\metre\\tothe{0.333333}', // fractional exponent ~ one-third power
'\\metre\\tothe{0.25}' // fractional exponent ~ one-quarter power
];
// Some invalid examples.
const invalidDSI = [
'\\foo', // Unknown unit.
'\\milli\\tothe{2}', // Missing base unit after prefix.
'\\metre\\per', // Fraction missing numerator or denominator.
'\\\\metre\\per\\second' // Double backslashes.
];
// This function creates a new DSIUnit instance from a raw string,
// calls its toHTML() method, and returns an object with the rendered HTML and warnings.
function renderDSI(rawStr) {
try {
const dsi = new DSIUnit(rawStr);
return {
html: dsi.toHTML(),
warnings: dsi.warnings
};
} catch (error) {
return {
html: `<span style="color:red;">Error: ${error.message}</span>`,
warnings: [error.message]
};
}
}
// Populate the given table (by its ID) with rows for each raw string.
function populateTable(tableId, dsiStrings) {
const tbody = document.getElementById(tableId).querySelector('tbody');
dsiStrings.forEach(rawStr => {
const result = renderDSI(rawStr);
const tr = document.createElement('tr');
// Raw string in teletype
const tdRaw = document.createElement('td');
tdRaw.innerHTML = `<pre>${rawStr}</pre>`;
tr.appendChild(tdRaw);
// Rendered output
const tdRendered = document.createElement('td');
tdRendered.innerHTML = result.html;
tr.appendChild(tdRendered);
// If this is the invalid table, add a warnings column.
if (tableId === 'invalid-table') {
const tdWarnings = document.createElement('td');
tdWarnings.innerHTML = result.warnings.join('<br>');
tr.appendChild(tdWarnings);
}
tbody.appendChild(tr);
});
}
// Populate both tables.
populateTable('valid-table', validDSI);
populateTable('invalid-table', invalidDSI);
</script>
</body> </body>
</html> </html>
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment