Universal metric translator

Automatically converts imperial units to metric units

当前为 2017-07-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Universal metric translator
  3. // @namespace https://bennyjacobs.nl/userscripts/Universal-metric-translator
  4. // @description Automatically converts imperial units to metric units
  5. // @include about:addons
  6. // @include http*
  7. // @include https*
  8. // @version 2.1.1
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. // Flags: global, insensitive
  13. var createTransformationRegEx = function(unit) {
  14. return new RegExp(
  15. '\\s?'
  16.  
  17. + '((?:\\d+(?:,\\d+)*)(?:\\.\\d+)?' // eg 9.23 or 23 or 0.34 or 2,204.6
  18. + '|\\d*(?:\\.\\d+)' // eg .34 but not 0.34
  19. + '|(?:\\d+(?:,\\d+)*\\s)?\\d+(?:,\\d+)*/\\d+(?:,\\d+)*)' // 1 1/2 or 1/4, common with imperial
  20.  
  21. + '(?:\\s*' + unit
  22. + '\\b(?!(\\s\\[|\\]))' // prevent infinite replacement
  23. // + '|\(?=\\s*(?:to|and|-)[\\d\\./\\s]+' + unit + '\\b)'
  24. + ')'
  25. ,"gi");
  26. };
  27.  
  28. // Sources:
  29. // https://en.wikipedia.org/wiki/Imperial_units
  30. // https://en.wikipedia.org/wiki/Metre
  31. // https://en.wikipedia.org/wiki/Square_metre
  32. // https://en.wikipedia.org/wiki/Litre
  33. var tranformationTable = [
  34.  
  35. // Temperature
  36. {
  37. from: '(?:F|fahrenheit|fahrenheits|degrees F|degrees fahrenheit)',
  38. to: '℃',
  39. convert: function(fahrenheits){
  40. return ((fahrenheits - 32) / 1.8).toFixed(2);
  41. }
  42. },
  43.  
  44. // Distance
  45. {
  46. from: 'thou',
  47. to: 'm',
  48. convert: 25.4 * 1e-6,
  49. }, {
  50. from: '(?:inch(?:es|e)?)',
  51. to: 'm',
  52. convert: 25.4 * 1e-3,
  53. }, {
  54. from: '(?:(?:feets?|foot))',
  55. to: 'm',
  56. convert: 0.3048,
  57. }, {
  58. from: '(?:yards?|yd)',
  59. to: 'm',
  60. convert: 0.9144,
  61. }, {
  62. from: 'chains?',
  63. to: 'm',
  64. convert: 20.1168,
  65. }, {
  66. from: '(?:furlongs?|fur)',
  67. to: 'm',
  68. convert: 201.168,
  69. }, {
  70. from: 'miles?',
  71. to: 'm',
  72. convert: 1.609344 * 1e3,
  73. }, {
  74. from: 'leagues?',
  75. to: 'm',
  76. convert: 4.828032 * 1e3,
  77. },
  78.  
  79. // Maritime distances
  80. {
  81. from: '(?:fathoms?|ftm)',
  82. to: 'm',
  83. convert: 1.853184,
  84. }, {
  85. from: 'cables?',
  86. to: 'm',
  87. convert: 185.3184,
  88. }, {
  89. from: 'nautical\\smiles?', // Note: two backslashes as we are escaping a javascript string
  90. to: 'm',
  91. convert: 1.853184 * 1e3,
  92. },
  93.  
  94. // Gunter's survey units (17th century onwards)
  95. {
  96. from: 'link',
  97. to: 'm',
  98. convert: 0.201168,
  99. }, {
  100. from: 'rod',
  101. to: 'm',
  102. convert: 5.0292,
  103. }, {
  104. from: 'chain',
  105. to: 'm',
  106. convert: 20.1168,
  107. },
  108.  
  109. // Area
  110. {
  111. from: 'acres?',
  112. to: 'km²',
  113. convert: 4.0468564224,
  114. },
  115.  
  116. // Volume
  117. {
  118. from: '(?:fluid ounces?|fl oz)',
  119. to: 'L',
  120. convert: 28.4130625 * 1e-3,
  121. }, {
  122. from: 'gill?',
  123. to: 'L',
  124. convert: 142.0653125 * 1e-3,
  125. }, {
  126. from: '(?:pints?|pt)',
  127. to: 'L',
  128. convert: 0.56826125,
  129. }, {
  130. from: 'quarts?',
  131. to: 'L',
  132. convert: 1.1365225,
  133. }, {
  134. from: 'gal(?:lons?)?',
  135. to: 'L',
  136. convert: 4.54609,
  137. },
  138.  
  139. //Weight
  140. {
  141. from: 'grains?',
  142. to: 'g',
  143. convert: 64.79891 * 1e-3,
  144. }, {
  145. from: 'drachm',
  146. to: 'g',
  147. convert: 1.7718451953125,
  148. }, {
  149. from: '(?:ounces?|oz)',
  150. to: 'g',
  151. convert: 28.349523125,
  152. }, {
  153. // from: 'lbs?|pounds?', // Pound is ambiguous. It can be a currency. Therefore we don't touch it.
  154. // Actually, since it would be displayed as
  155. // "It costs 1 pound [453.59 g]."
  156. // the metric translation can just be ignored by the reader.
  157. // I'm leaving it out anyways. lbs is usually used in written text anyways so it covers most cases.
  158. from: 'lbs?',
  159. to: 'g',
  160. convert: 453.59,
  161. }, {
  162. from: 'stones?',
  163. to: 'g',
  164. convert: 6.35029318 * 1e3,
  165. }, {
  166. from: 'quarters?',
  167. to: 'g',
  168. convert: 12.70058636 * 1e3,
  169. }, {
  170. from: 'hundredweights?',
  171. to: 'g',
  172. convert: 50.80234544 * 1e3,
  173. },
  174. // A 'ton' might belong here, but there exist a metric ton and a imperial ton.
  175. // Qon commment: A metric ton is sometimes spelled metric tonne or just tonne though.
  176. // https://en.wikipedia.org/wiki/Ton
  177. ];
  178.  
  179. tranformationTable.forEach(function (transformationRule) {
  180. transformationRule.regex = createTransformationRegEx(transformationRule.from);
  181. });
  182.  
  183. var replaceSubstring = function(originalText, index, length, replacement) {
  184. var before_substring = originalText.substring(0, index);
  185. var after_substring = originalText.substring(index+length);
  186. return before_substring + replacement + after_substring;
  187. };
  188.  
  189. function round_number(num, dec) {
  190. return Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
  191. }
  192.  
  193. // The transformText function is idempotent.
  194. // Repeated calls on the output will do nothing. Only the first invocation has any effect.
  195. // The input will be returned on repeated calls.
  196. var transformText = function(text) {
  197. tranformationTable.forEach(function (transformationRule) {
  198. transformationRule.regex.lastIndex = 0;
  199. for(var match; match = transformationRule.regex.exec(text);) {
  200.  
  201. // console.log(match, parseFloat(match[1], 10))
  202. var old_value, new_value;
  203.  
  204. // if the number is written like 1 1/4 instead of 1.25 then:
  205. if(/\//.test(match[1])) {
  206. old_value = match[1].split(' ')
  207. if(old_value.length == 2)
  208. {
  209. var a = old_value[1].split('/')
  210. old_value[1] = parseFloat(a[0].replace(/,/g, ''), 10) / parseFloat(a[1].replace(/,/g, ''), 10)
  211. old_value = parseFloat(old_value[0].replace(/,/g, ''), 10) + old_value[1]
  212. }
  213. else
  214. {
  215. var a = old_value[0].split('/')
  216. old_value = parseFloat(a[0].replace(/,/g, ''), 10) / parseFloat(a[1].replace(/,/g, ''), 10)
  217. }
  218. } else {
  219. old_value = parseFloat(match[1].replace(/,/g, ''), 10)
  220. }
  221. if(typeof transformationRule.convert == 'function') {
  222. new_value = transformationRule.convert(old_value);
  223. } else {
  224. new_value = old_value * transformationRule.convert;
  225. }
  226. var new_unit = transformationRule.to;
  227. if(new_unit === 'g' || new_unit === 'L' || new_unit === 'm')
  228. {
  229. if(new_value > 1e12) {
  230. new_unit = 'T' + new_unit
  231. new_value /= 1e12
  232. } else if (new_value > 1e9) {
  233. new_unit = 'G' + new_unit
  234. new_value /= 1e9
  235. } else if (new_value > 1e6) {
  236. // if(new_unit === 'g') new_unit = 'tonne' else
  237. new_unit = 'M' + new_unit
  238. new_value /= 1e6
  239. } else if (new_value > 1e3) {
  240. new_unit = 'k' + new_unit
  241. new_value /= 1e3
  242. } else if (new_value < 1e-9) {
  243. new_unit = 'p' + new_unit
  244. new_value /= 1e-12
  245. } else if (new_value < 1e-6) {
  246. new_unit = 'n' + new_unit
  247. new_value /= 1e-9
  248. } else if (new_value < 1e-3) {
  249. new_unit = 'µ' + new_unit
  250. new_value /= 1e-6
  251. } else if (new_value < 1e-2) {
  252. new_unit = 'm' + new_unit
  253. new_value /= 1e-3
  254. } else if (new_value < 1 && (new_unit !== 'g')) {
  255. new_unit = 'c' + new_unit
  256. new_value /= 1e-2
  257. }
  258. }
  259. // function significantDigits(old, new) {
  260. // old.replace(/^[^1-9]*/, '').replace(/\D/g, '').length
  261. // }
  262. new_value = round_number(new_value, 2)
  263. if(true) {
  264. var new_substring =
  265. match[0]
  266. + ' ['
  267. + new_value
  268. + " "
  269. + new_unit
  270. + ']'
  271. } else {
  272. var new_substring =
  273. new_value
  274. + " "
  275. + new_unit
  276. + ' ['
  277. + match[0]
  278. + ']'
  279. }
  280.  
  281. text = replaceSubstring(text, match.index, match[0].length, new_substring );
  282. // Move the matching index past whatever we have replaced.
  283. // Note: The replacement can be shorter or longer.
  284. transformationRule.regex.lastIndex = transformationRule.regex.lastIndex + (new_substring.length - match[0].length);
  285. }
  286. });
  287. return text;
  288. };
  289.  
  290. // conversation => convert. Because this has nothing to do with discussions.
  291. // Rounding moved so it happens only immediatly before being inserted into
  292. // the document.
  293. // If it's done as the first step we lose a lot(!) of precision.
  294. // As an example 0.004 inches was rounded to 0, then converted to metric
  295. // (still 0) and then inserted. Extremely wrong.
  296. // Also 0.01497 miles gets rounded to 0.01 (33% less!) and then converted to
  297. // metric. The result is 0.01 * 1.609344 = 0.01609344, way more digits than
  298. // before we started! This looks extremely precise with 8 significant digits,
  299. // but it's actually only 1 since began by completely destroying our initial
  300. // number. Correct convertion should have the same number of significant
  301. // digits as the initial number (4). But some numbers, like 1 inch, might
  302. // actually be exactly 1 inch, or 2.54 cm. Rounding 1 inch to 3 cm seems a
  303. // bit wrong. It's unusual that 1 inch is written like "1.00 inches" even
  304. // if that precision is intended. So a flat rounding to 2 decimals after(!)
  305. // choosing a good prefix (and scaling our number by the prefix) should work
  306. // for most cases.
  307.  
  308. var handleTextNode = function(textNode) {
  309. var transformedText = transformText(textNode.nodeValue);
  310. if(textNode.nodeValue != transformedText)
  311. textNode.nodeValue = transformedText;
  312. };
  313.  
  314. // Travel the node(s) in a recursive fashion.
  315. var walk = function(node) {
  316. var child, next;
  317.  
  318. switch (node.nodeType) {
  319. case 1: // Element
  320. case 9: // Document
  321. case 11: // Document fragment
  322. child = node.firstChild;
  323. while (child) {
  324. next = child.nextSibling;
  325. walk(child);
  326. child = next;
  327. }
  328. break;
  329. case 3: // Text node
  330. handleTextNode(node);
  331. break;
  332. default:
  333. break;
  334. }
  335. };
  336.  
  337. var MutationObserver = (window.MutationObserver || window.WebKitMutationObserver);
  338. var observer = new MutationObserver(function (mutations) {
  339. mutations.forEach(function (mutation) {
  340. if(mutation.type == 'childList') {
  341. for (var i = 0; i < mutation.addedNodes.length; ++i) {
  342. walk(mutation.addedNodes[i]);
  343. }
  344. } else if (mutation.type == 'characterData') {
  345. handleTextNode(mutation.target);
  346. }
  347. });
  348. });
  349.  
  350. observer.observe(document, {
  351. childList: true,
  352. characterData: true,
  353. subtree: true,
  354. });
  355.  
  356. walk(document.body);