333 lines
12 KiB
JavaScript
333 lines
12 KiB
JavaScript
DDH.roman = DDH.roman || {};
|
|
|
|
(function(DDH) {
|
|
"use strict";
|
|
|
|
/* Reference: https://en.wikipedia.org/wiki/Roman_numerals
|
|
* Inputs can be between 1 and 3999.
|
|
* We rely on the substractive notation.
|
|
*/
|
|
|
|
var romanTable = {
|
|
'I': 1,
|
|
'V': 5,
|
|
'X': 10,
|
|
'L': 50,
|
|
'C': 100,
|
|
'D': 500,
|
|
'M': 1000
|
|
};
|
|
|
|
/* The arabicTable is used to convert an arabic digit into roman characters.
|
|
* The field 'before' contains the substractive notation. Hence, we need
|
|
* specific cases when converting to roman notation for the digits 4 and 9.
|
|
*/
|
|
|
|
var arabicTable = {
|
|
1: {digit: 'I', before: 'IV'},
|
|
5: {digit: 'V'},
|
|
10: {digit: 'X', before: 'XL'},
|
|
50: {digit: 'L'},
|
|
100: {digit: 'C', before: 'CD'},
|
|
500: {digit: 'D'},
|
|
1000: {digit: 'M'}
|
|
};
|
|
|
|
/* validationTable and conversionTable are used to avoid nested conditionals
|
|
* in the conversion process.
|
|
*/
|
|
|
|
var validationTable = {
|
|
'roman': isRoman,
|
|
'arabic': isArabic
|
|
};
|
|
|
|
var rangeTable = {
|
|
'roman': isRomanNumberInRange,
|
|
'arabic': isArabicNumberInRange
|
|
};
|
|
|
|
var conversionTable = {
|
|
'roman': { 'arabic': romanToArabic },
|
|
'arabic': { 'roman': arabicToRoman }
|
|
};
|
|
|
|
/* Roman regular expression
|
|
*
|
|
* It is composed of 4 blocks:
|
|
* - M{0,3} -> from M to MMM
|
|
* - (?:D?C{0,3}|C[DM]) -> from C to CM
|
|
* - (?:L?X{0,3}|X[LC]) -> from X to XC
|
|
* - (?:V?I{0,3}|I[VX]) -> from I to IX
|
|
*/
|
|
|
|
function isRoman(input) {
|
|
var r = /^M{0,3}(?:D?C{0,3}|C[DM])(?:L?X{0,3}|X[LC])(?:V?I{0,3}|I[VX])$/i;
|
|
return input != '' &&
|
|
input.match(r);
|
|
}
|
|
|
|
/* isRomanNumberInRange expects a non-null string representing a
|
|
* roman number.
|
|
*
|
|
* It always returns true because a valid roman number is in range.
|
|
*/
|
|
|
|
function isRomanNumberInRange(input) {
|
|
return true;
|
|
}
|
|
|
|
/* romanToArabic expects a non-null string representing a roman number.
|
|
*
|
|
* The algorithm relies on the order of the characters. If c is the
|
|
* current character, then there are two cases: Either, c is smaller
|
|
* than the previous character we add the value of the roman character.
|
|
* Or, c is greater than the previous character. In this case, we are
|
|
* in a substractive notation context. So we substract the previously
|
|
* added value and we also substract the value of the previous value to
|
|
* the value of the current character (hence the -2 * ...).
|
|
* The first previous value is initialized to the greatest possible value,
|
|
* 1000, to ensure the additive notation context at the beginning.
|
|
*/
|
|
|
|
function romanToArabic(roman) {
|
|
var romanDigits = roman.toUpperCase().split('');
|
|
var previousArabicValue = 1000;
|
|
|
|
return romanDigits.reduce(function(acc, romanDigit) {
|
|
var arabicValue = romanTable[romanDigit];
|
|
var nextAcc = acc;
|
|
|
|
if (arabicValue <= previousArabicValue) {
|
|
nextAcc += arabicValue;
|
|
} else {
|
|
nextAcc += -2 * previousArabicValue + arabicValue;
|
|
}
|
|
previousArabicValue = arabicValue;
|
|
|
|
return nextAcc;
|
|
}, 0);
|
|
}
|
|
|
|
function isArabic(input) {
|
|
return input != '0' && input.match(/\d+/);
|
|
}
|
|
|
|
/* isArabicNumberInRange expects an integer.
|
|
*
|
|
* We limit the valid range to [1,3999] because the greatest roman
|
|
* number represented by ascii characters is MMMCMXCIX (3999).
|
|
*/
|
|
|
|
function isArabicNumberInRange(input) {
|
|
var number = parseInt(input);
|
|
return number >= 1 && number <= 3999;
|
|
}
|
|
|
|
/* arabicToRoman expects a non-null string composed of digits.
|
|
*
|
|
* Because of the restriction on roman numbers, we limit arabic numbers to
|
|
* 3999.
|
|
*
|
|
* Arabic numbers use base 10 and can be expressed by the following
|
|
* expression n = a x 10^3 + b x 10^2 + c x 10 + d where a, b, c, d
|
|
* are digits between 0 and 9. Hence, we need to consider specific
|
|
* cases to convert to roman numbers.
|
|
* The process goes from left to right.
|
|
* If the value of the current digit is 4 or 9, we need to switch to
|
|
* the substractive notation.
|
|
* If the value of the current digit is 5, we need to use the right
|
|
* component in the roman notation (V, L, D).
|
|
* The remaining cases are straightforward.
|
|
*/
|
|
|
|
function arabicToRoman(arabic) {
|
|
var arabicValue = parseInt(arabic);
|
|
var previousComponent;
|
|
var romanDigits = '';
|
|
|
|
var components = [1000, 100, 10, 1];
|
|
for (var i = 0; i < components.length; i++) {
|
|
var leftDigit = Math.floor(arabicValue / components[i]);
|
|
|
|
if (leftDigit >= 1 && leftDigit <= 3) {
|
|
var romanDigit = arabicTable[components[i]].digit.repeat(leftDigit);
|
|
romanDigits += romanDigit;
|
|
} else if (leftDigit == 4) {
|
|
var romanDigit = arabicTable[components[i]].before;
|
|
romanDigits += romanDigit;
|
|
} else if (leftDigit == 5) {
|
|
var romanDigit = arabicTable[components[i] * 5].digit;
|
|
romanDigits += romanDigit;
|
|
} else if (leftDigit >= 6 && leftDigit <= 8) {
|
|
var romanDigit = arabicTable[components[i] * 5].digit;
|
|
var end = arabicTable[components[i]].digit.repeat(leftDigit - 5);
|
|
romanDigits += romanDigit + end;
|
|
} else if (leftDigit == 9) {
|
|
var romanDigit = arabicTable[previousComponent].digit;
|
|
romanDigits += arabicTable[components[i]].digit + romanDigit;
|
|
} else {
|
|
/* if 0 then there is nothing to do. */
|
|
}
|
|
|
|
previousComponent = components[i];
|
|
arabicValue -= components[i] * leftDigit;
|
|
};
|
|
|
|
return romanDigits;
|
|
}
|
|
|
|
function buildConverter(input, output) {
|
|
var $root = DDH.getDOM('roman');
|
|
|
|
var $arabicWarning = $root.find('.converter__messages__arabic');
|
|
var $romanWarning = $root.find('.converter__messages__roman');
|
|
|
|
/* The following four closures capture the two previous DOM elements.
|
|
* Hence, we build a warning table to display the right information.
|
|
* Warnings are displayed by making the font weight bold and increasing
|
|
* the font size.
|
|
*/
|
|
|
|
function raiseArabicWarning() {
|
|
$arabicWarning.css('font-weight', 'bold');
|
|
$arabicWarning.css('font-size', '1.5em');
|
|
}
|
|
|
|
function unraiseArabicWarning() {
|
|
$arabicWarning.css('font-weight', 'normal');
|
|
$arabicWarning.css('font-size', '1em');
|
|
}
|
|
|
|
function raiseRomanWarning() {
|
|
$romanWarning.css('font-weight', 'bold');
|
|
$romanWarning.css('font-size', '1.5em');
|
|
}
|
|
|
|
function unraiseRomanWarning() {
|
|
$romanWarning.css('font-weight', 'normal');
|
|
$romanWarning.css('font-size', '1em');
|
|
}
|
|
|
|
var raiseWarningTable = {
|
|
'roman': raiseRomanWarning,
|
|
'arabic': raiseArabicWarning
|
|
};
|
|
|
|
var unraiseWarningTable = {
|
|
'roman': unraiseRomanWarning,
|
|
'arabic': unraiseArabicWarning
|
|
};
|
|
|
|
return {
|
|
inputLabel: DDG.capitalize(input),
|
|
isInputValid: validationTable[input],
|
|
isInputInRange: rangeTable[input],
|
|
input: $root.find('.converter__input__field'),
|
|
raiseInputWarning: raiseWarningTable[input],
|
|
unraiseInputWarning: unraiseWarningTable[input],
|
|
outputLabel: DDG.capitalize(output),
|
|
isOutputValid: validationTable[output],
|
|
isOutputInRange: rangeTable[output],
|
|
output: $root.find('.converter__output__field'),
|
|
raiseOutputWarning: raiseWarningTable[output],
|
|
unraiseOutputWarning: unraiseWarningTable[output],
|
|
inputToOutput: conversionTable[input][output],
|
|
outputToInput: conversionTable[output][input]
|
|
};
|
|
}
|
|
|
|
DDH.roman.build = function(ops) {
|
|
var input = ops.data.input;
|
|
var output = ops.data.output;
|
|
var inputValue = ops.data.input_value;
|
|
var outputValue = ops.data.output_value;
|
|
|
|
/* State variables.
|
|
* initiated: avoid init logic running multiple times.
|
|
* warning: has a warning been raised?
|
|
*/
|
|
|
|
var initiated = false;
|
|
var warning = false;
|
|
|
|
return {
|
|
onShow: function() {
|
|
/* Mechanism to avoid the init logic running multiple times. */
|
|
if (initiated) {
|
|
return;
|
|
}
|
|
|
|
var $converter = buildConverter(input, output);
|
|
|
|
$('.converter__input__label').html($converter.inputLabel);
|
|
$('.converter__output__label').html($converter.outputLabel);
|
|
$converter.input.val(inputValue);
|
|
$converter.output.val(outputValue);
|
|
|
|
/* Just in case the input provided by the server was out
|
|
* of range. In this case, we immediately raise a warning.
|
|
*/
|
|
|
|
if (! $converter.isInputInRange()) {
|
|
warning = true;
|
|
$converter.raiseInputWarning();
|
|
}
|
|
|
|
/* There are four cases to discriminate related to inputs.
|
|
*
|
|
* - empty input
|
|
* - syntactically incorrect input
|
|
* - out of range input
|
|
* - valid input
|
|
*/
|
|
|
|
$converter.input.keyup(function (e) {
|
|
var input = $converter.input.val();
|
|
if (input == '') {
|
|
$converter.output.val('');
|
|
} else if (! $converter.isInputValid(input)) {
|
|
$converter.output.val('');
|
|
} else if (! $converter.isInputInRange(input)) {
|
|
warning = true;
|
|
$converter.output.val('');
|
|
$converter.raiseInputWarning();
|
|
} else {
|
|
if (warning) {
|
|
warning = false;
|
|
$converter.unraiseInputWarning();
|
|
$converter.unraiseOutputWarning();
|
|
}
|
|
var output = $converter.inputToOutput(input);
|
|
$converter.output.val(output);
|
|
}
|
|
});
|
|
|
|
$converter.output.keyup(function (e) {
|
|
var output = $converter.output.val();
|
|
|
|
if (output == '') {
|
|
$converter.input.val('');
|
|
} else if (! $converter.isOutputValid(output)) {
|
|
$converter.input.val('');
|
|
} else if (! $converter.isOutputInRange(output)) {
|
|
warning = true;
|
|
$converter.input.val('');
|
|
$converter.raiseOutputWarning();
|
|
} else {
|
|
if (warning) {
|
|
warning = false;
|
|
$converter.unraiseOutputWarning();
|
|
$converter.unraiseInputWarning();
|
|
}
|
|
var input = $converter.outputToInput(output);
|
|
$converter.input.val(input);
|
|
}
|
|
});
|
|
|
|
initiated = true;
|
|
}
|
|
};
|
|
};
|
|
|
|
})(DDH); |