RosettaCodeData/Task/Box-the-compass/JavaScript/box-the-compass-2.js

240 lines
7.3 KiB
JavaScript

(() => {
'use strict';
// GENERIC FUNCTIONS
// toTitle :: String -> String
let toTitle = s => s.length ? (s[0].toUpperCase() + s.slice(1)) : '';
// COMPASS DATA AND FUNCTIONS
// Scale invariant keys for points of the compass
// (allows us to look up a translation for one scale of compass (32 here)
// for use in another size of compass (8 or 16 points)
// (Also semi-serviceable as more or less legible keys without translation)
// compassKeys :: Int -> [String]
let compassKeys = depth => {
let urCompass = ['N', 'S', 'N'],
subdivision = (compass, n) => n <= 1 ? (
compass
) : subdivision( // Borders between N and S engender E and W.
// other new boxes concatenate their parent keys.
compass.reduce((a, x, i, xs) => {
if (i > 0) {
return (n === depth) ? (
a.concat([x === 'N' ? 'W' : 'E'], x)
) : a.concat([xs[i - 1] + x, x]);
} else return a.concat(x);
}, []),
n - 1
);
return subdivision(urCompass, depth)
.slice(0, -1);
};
// https://zh.wikipedia.org/wiki/%E7%BD%97%E7%9B%98%E6%96%B9%E4%BD%8D
let lstLangs = [{
'name': 'English',
expansions: {
N: 'north',
S: 'south',
E: 'east',
W: 'west',
b: ' by ',
'-': '-'
},
'N': 'N',
'NNNE': 'NbE',
'NNE': 'N-NE',
'NNENE': 'NEbN',
'NE': 'NE',
'NENEE': 'NEbE',
'NEE': 'E-NE',
'NEEE': 'EbN',
'E': 'E',
'EEES': 'EbS',
'EES': 'E-SE',
'EESES': 'SEbE',
'ES': 'SE',
'ESESS': 'SEbS',
'ESS': 'S-SE',
'ESSS': 'SbE',
'S': 'S',
'SSSW': 'SbW',
'SSW': 'S-SW',
'SSWSW': 'SWbS',
'SW': 'SW',
'SWSWW': 'SWbW',
'SWW': 'W-SW',
'SWWW': 'WbS',
'W': 'W',
'WWWN': 'WbN',
'WWN': 'W-NW',
'WWNWN': 'NWbW',
'WN': 'NW',
'WNWNN': 'NWbN',
'WNN': 'N-NW',
'WNNN': 'NbW'
}, {
'name': 'Chinese',
'N': '北',
'NNNE': '北微东',
'NNE': '东北偏北',
'NNENE': '东北微北',
'NE': '东北',
'NENEE': '东北微东',
'NEE': '东北偏东',
'NEEE': '东微北',
'E': '东',
'EEES': '东微南',
'EES': '东南偏东',
'EESES': '东南微东',
'ES': '东南',
'ESESS': '东南微南',
'ESS': '东南偏南',
'ESSS': '南微东',
'S': '南',
'SSSW': '南微西',
'SSW': '西南偏南',
'SSWSW': '西南微南',
'SW': '西南',
'SWSWW': '西南微西',
'SWW': '西南偏西',
'SWWW': '西微南',
'W': '西',
'WWWN': '西微北',
'WWN': '西北偏西',
'WWNWN': '西北微西',
'WN': '西北',
'WNWNN': '西北微北',
'WNN': '西北偏北',
'WNNN': '北微西'
}];
// pointIndex :: Int -> Num -> Int
let pointIndex = (power, degrees) => {
let nBoxes = (power ? Math.pow(2, power) : 32);
return Math.ceil(
(degrees + (360 / (nBoxes * 2))) % 360 * nBoxes / 360
) || 1;
};
// pointNames :: Int -> Int -> [String]
let pointNames = (precision, iBox) => {
let k = compassKeys(precision)[iBox - 1];
return lstLangs.map(dctLang => {
let s = dctLang[k] || k, // fallback to key if no translation
dctEx = dctLang.expansions;
return dctEx ? toTitle(s.split('')
.map(c => dctEx[c])
.join(precision > 5 ? ' ' : ''))
.replace(/ /g, ' ') : s;
});
};
// maximumBy :: (a -> a -> Ordering) -> [a] -> a
let maximumBy = (f, xs) =>
xs.reduce((a, x) => a === undefined ? x : (
f(x, a) > 0 ? x : a
), undefined);
// justifyLeft :: Int -> Char -> Text -> Text
let justifyLeft = (n, cFiller, strText) =>
n > strText.length ? (
(strText + replicate(n, cFiller)
.join(''))
.substr(0, n)
) : strText;
// justifyRight :: Int -> Char -> Text -> Text
let justifyRight = (n, cFiller, strText) =>
n > strText.length ? (
(replicate(n, cFiller)
.join('') + strText)
.slice(-n)
) : strText;
// replicate :: Int -> a -> [a]
let replicate = (n, a) => {
let v = [a],
o = [];
if (n < 1) return o;
while (n > 1) {
if (n & 1) o = o.concat(v);
n >>= 1;
v = v.concat(v);
}
return o.concat(v);
};
// transpose :: [[a]] -> [[a]]
let transpose = xs =>
xs[0].map((_, iCol) => xs.map((row) => row[iCol]));
// length :: [a] -> Int
// length :: Text -> Int
let length = xs => xs.length;
// compareByLength = (a, a) -> (-1 | 0 | 1)
let compareByLength = (a, b) => {
let [na, nb] = [a, b].map(length);
return na < nb ? -1 : na > nb ? 1 : 0;
};
// maxLen :: [String] -> Int
let maxLen = xs => maximumBy(compareByLength, xs)
.length;
// compassTable :: Int -> [Num] -> Maybe String
let compassTable = (precision, xs) => {
if (precision < 1) return undefined;
else {
let intPad = 2;
let lstIndex = xs.map(x => pointIndex(precision, x)),
lstStrIndex = lstIndex.map(x => x.toString()),
nIndexWidth = maxLen(lstStrIndex),
colIndex = lstStrIndex.map(
x => justifyRight(nIndexWidth, ' ', x)
);
let lstAngles = xs.map(x => x.toFixed(2) + '°'),
nAngleWidth = maxLen(lstAngles) + intPad,
colAngles = lstAngles.map(x => justifyRight(nAngleWidth, ' ', x));
let lstTrans = transpose(
lstIndex.map(i => pointNames(precision, i))
),
lstTransWidths = lstTrans.map(x => maxLen(x) + 2),
colsTrans = lstTrans
.map((lstLang, i) => lstLang
.map(x => justifyLeft(lstTransWidths[i], ' ', x))
);
return transpose([colIndex]
.concat([colAngles], [replicate(lstIndex.length, " ")])
.concat(colsTrans))
.map(x => x.join(''))
.join('\n');
}
}
// TEST
let xs = [0.0, 16.87, 16.88, 33.75, 50.62, 50.63, 67.5, 84.37,
84.38, 101.25, 118.12, 118.13, 135.0, 151.87, 151.88, 168.75,
185.62, 185.63, 202.5, 219.37, 219.38, 236.25, 253.12, 253.13,
270.0, 286.87, 286.88, 303.75, 320.62, 320.63, 337.5, 354.37,
354.38
];
// If we supply other precisions, like 4 or 6, (2^n -> 16 or 64 boxes)
// the bearings will be divided amongst smaller or larger numbers of boxes,
// either using name translations retrieved by the generic hash
// or using the hash itself (combined with any expansions)
// to substitute for missing names for very finely divided boxes.
return compassTable(5, xs); // 2^5 -> 32 boxes
})();