├── .gitignore ├── esbuild.js ├── package.json ├── LICENCE ├── approx_fraction.mjs ├── README.md ├── oddslib.mjs └── test └── index.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /npm-debug.log 3 | .tern-port 4 | dist 5 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild'); 2 | 3 | const common = { 4 | entryPoints: ['./oddslib.mjs'], 5 | bundle: true, 6 | platform: 'node', 7 | }; 8 | 9 | build({ 10 | ...common, 11 | outfile: 'dist/oddslib.cjs.js', 12 | }); 13 | 14 | build({ 15 | ...common, 16 | outfile: 'dist/oddslib.esm.js', 17 | format: 'esm' 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oddslib", 3 | "version": "2.1.1", 4 | "description": "Odds conversion and formatting library", 5 | "main": "dist/oddslib.cjs.js", 6 | "module": "dist/oddslib.esm.js", 7 | "scripts": { 8 | "build": "node ./esbuild.js", 9 | "test": "mocha --reporter spec" 10 | }, 11 | "author": "Stéphane Travostino ", 12 | "license": "MIT", 13 | "keywords": [ 14 | "odds", 15 | "conversion", 16 | "formatting", 17 | "european", 18 | "fractional", 19 | "uk", 20 | "indonesian", 21 | "american", 22 | "malay", 23 | "indonesian", 24 | "malaysia", 25 | "probability", 26 | "implied probability", 27 | "decimal" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/1player/oddslib" 32 | }, 33 | "devDependencies": { 34 | "chai": "^4.3.4", 35 | "esbuild": "^0.12.19", 36 | "mocha": "^10.1.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Stéphane Travostino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /approx_fraction.mjs: -------------------------------------------------------------------------------- 1 | // From http://www.mindspring.com/~alanh/fracs.html 2 | 3 | // Approximate decimal number `d` into the returned fraction `frac`, 4 | // so that |frac - d| < (10^-precision / 2). 5 | // In other words, return an approximate fraction correct up to the 6 | // `precision`th decimal place. 7 | export default function approximateFraction(d, precision) { 8 | var numerators = [0, 1]; 9 | var denominators = [1, 0]; 10 | 11 | var maxNumerator = getMaxNumerator(d); 12 | var d2 = d; 13 | var calcD, 14 | prevCalcD = NaN; 15 | 16 | var acceptableError = Math.pow(10, -precision) / 2; 17 | 18 | for (var i = 2; i < 1000; i++) { 19 | var L2 = Math.floor(d2); 20 | numerators[i] = L2 * numerators[i - 1] + numerators[i - 2]; 21 | if (Math.abs(numerators[i]) > maxNumerator) return; 22 | 23 | denominators[i] = L2 * denominators[i - 1] + denominators[i - 2]; 24 | 25 | calcD = numerators[i] / denominators[i]; 26 | 27 | if (Math.abs(calcD - d) < acceptableError || calcD == prevCalcD) { 28 | return numerators[i].toString() + "/" + denominators[i].toString(); 29 | } 30 | 31 | d2 = 1 / (d2 - L2); 32 | } 33 | } 34 | 35 | function getMaxNumerator(f) { 36 | var f2 = null; 37 | var ixe = f.toString().indexOf("E"); 38 | if (ixe == -1) ixe = f.toString().indexOf("e"); 39 | if (ixe == -1) f2 = f.toString(); 40 | else f2 = f.toString().substring(0, ixe); 41 | 42 | var digits = null; 43 | var ix = f2.toString().indexOf("."); 44 | if (ix == -1) digits = f2; 45 | else if (ix === 0) digits = f2.substring(1, f2.length); 46 | else if (ix < f2.length) 47 | digits = f2.substring(0, ix) + f2.substring(ix + 1, f2.length); 48 | 49 | var L = digits; 50 | 51 | var numDigits = L.toString().length; 52 | var L2 = f; 53 | var numIntDigits = L2.toString().length; 54 | if (L2 === 0) numIntDigits = 0; 55 | var numDigitsPastDecimal = numDigits - numIntDigits; 56 | 57 | var i; 58 | for (i = numDigitsPastDecimal; i > 0 && L % 2 === 0; i--) L /= 2; 59 | for (i = numDigitsPastDecimal; i > 0 && L % 5 === 0; i--) L /= 5; 60 | 61 | return L; 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This library is stable and being used in production (as of Apr 2021).** 2 | 3 | A JS library to convert and format odds. 4 | 5 | # Usage 6 | 7 | Odds are constructed by calling the public `from(format, odds)` constructor: 8 | 9 | ```js 10 | import * as oddslib from 'oddslib'; 11 | 12 | // Create from decimal/European odds 13 | let odds = oddslib.from('decimal', 1.20); 14 | 15 | // Create from American/Moneyline odds 16 | let odds = oddslib.from('moneyline', -500); 17 | 18 | // Create from Hong Kong odds 19 | let odds = oddslib.from('hongKong', .20); 20 | 21 | // Create from implied probability 22 | let odds = oddslib.from('impliedProbability', .5) 23 | = oddslib.from('impliedProbability', '50%'); 24 | 25 | // Create from UK/fractional odds 26 | let odds = oddslib.from('fractional', 5/2) 27 | = oddslib.from('fractional', '5/2') 28 | = oddslib.from('fractional', 2.5); 29 | 30 | // Create from Malay odds 31 | let odds = oddslib.from('malay', 0.5); 32 | 33 | // Create from Indonesian odds 34 | let odds = oddslib.from('indonesian', -5.0); 35 | 36 | ``` 37 | 38 | Odds can then be converted using the `to(format, options?)` instance method: 39 | 40 | ```js 41 | let odds = oddslib.from('decimal', 1.20); 42 | 43 | odds.to('decimal'); // == 1.2 44 | odds.to('moneyline'); // == -500 45 | odds.to('hongKong'); // == 0.2 46 | odds.to('impliedProbability'); // == 0.83̅ 47 | odds.to('fractional'); // == "1/5" 48 | odds.to('malay'); // == 0.2 49 | odds.to('indonesian'); // == -5.0 50 | ``` 51 | 52 | The default options are: 53 | ```js 54 | { 55 | precision: null, // Return a rounded value correct to the nth digit. Do not round if null. 56 | percentage: false, // Return IP odds as a percentage string. No effect on other formats. 57 | } 58 | ``` 59 | 60 | Fractions are also approximated if the `precision` option is specified, by using the algorithm at [http://www.mindspring.com/~alanh/fracs.html](http://www.mindspring.com/~alanh/fracs.html). 61 | 62 | ```js 63 | // No approximation 64 | oddslib.from('decimal', 2.33).to('fractional'); // === "133/100" 65 | 66 | // Approximate up to 2nd decimal digit 67 | oddslib.from('decimal', 2.33).to('fractional', {precision: 2}); // === "4/3" 68 | ``` 69 | -------------------------------------------------------------------------------- /oddslib.mjs: -------------------------------------------------------------------------------- 1 | import approximateFraction from "./approx_fraction.mjs"; 2 | 3 | // Object.assign polyfill 4 | if (typeof Object.assign != "function") { 5 | Object.assign = function (target, varArgs) { 6 | // .length of function is 2 7 | "use strict"; 8 | if (target == null) { 9 | // TypeError if undefined or null 10 | throw new TypeError("Cannot convert undefined or null to object"); 11 | } 12 | 13 | var to = Object(target); 14 | 15 | for (var index = 1; index < arguments.length; index++) { 16 | var nextSource = arguments[index]; 17 | 18 | if (nextSource != null) { 19 | // Skip over if undefined or null 20 | for (var nextKey in nextSource) { 21 | // Avoid bugs when hasOwnProperty is shadowed 22 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 23 | to[nextKey] = nextSource[nextKey]; 24 | } 25 | } 26 | } 27 | } 28 | return to; 29 | }; 30 | } 31 | 32 | // 1.2 - 1.0 === 0.19999999999999996 33 | // fixFloatError(1.2 - 1.0) === 0.2 34 | var fixFloatError = function (n) { 35 | return parseFloat(n.toPrecision(12)); 36 | }; 37 | 38 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round 39 | function decimalAdjust(type, value, exp) { 40 | // If the exp is undefined or zero... 41 | if (typeof exp === "undefined" || +exp === 0) { 42 | return Math[type](value); 43 | } 44 | value = +value; 45 | exp = +exp; 46 | // If the value is not a number or the exp is not an integer... 47 | if (isNaN(value) || !(typeof exp === "number" && exp % 1 === 0)) { 48 | return NaN; 49 | } 50 | // If the value is negative... 51 | if (value < 0) { 52 | return -decimalAdjust(type, -value, exp); 53 | } 54 | // Shift 55 | value = value.toString().split("e"); 56 | value = Math[type](+(value[0] + "e" + (value[1] ? +value[1] - exp : -exp))); 57 | // Shift back 58 | value = value.toString().split("e"); 59 | return +(value[0] + "e" + (value[1] ? +value[1] + exp : exp)); 60 | } 61 | 62 | var FORMATS = { 63 | // European/Decimal format 64 | decimal: { 65 | from: function (decimal) { 66 | decimal = parseFloat(decimal); 67 | if (decimal <= 1.0) { 68 | throw new Error("Outside valid range."); 69 | } 70 | return decimal; 71 | }, 72 | to: function () { 73 | return this.decimalValue; 74 | }, 75 | }, 76 | 77 | // American/Moneyline format 78 | moneyline: { 79 | from: function (moneyline) { 80 | moneyline = parseFloat(moneyline); 81 | 82 | if (moneyline >= 0) { 83 | return moneyline / 100.0 + 1; 84 | } 85 | return 100 / -moneyline + 1; 86 | }, 87 | to: function () { 88 | if (this.decimalValue >= 2) { 89 | return fixFloatError((this.decimalValue - 1) * 100.0); 90 | } 91 | return fixFloatError(-100 / (this.decimalValue - 1)); 92 | }, 93 | }, 94 | 95 | // Hong Kong format 96 | hongKong: { 97 | from: function (hongKong) { 98 | hongKong = parseFloat(hongKong); 99 | if (hongKong < 0.0) { 100 | throw new Error("Outside valid range."); 101 | } 102 | return hongKong + 1.0; 103 | }, 104 | to: function () { 105 | return fixFloatError(this.decimalValue - 1); 106 | }, 107 | }, 108 | 109 | // Implied probability 110 | impliedProbability: { 111 | from: function (ip) { 112 | // Handle percentage string 113 | if (typeof ip === "string" && ip.slice(-1) == "%") { 114 | ip = parseFloat(ip) / 100.0; 115 | } else { 116 | ip = parseFloat(ip); 117 | } 118 | 119 | if (ip <= 0.0 || ip >= 1.0) { 120 | throw new Error("Outside valid range"); 121 | } 122 | 123 | return 1.0 / ip; 124 | }, 125 | to: function (options) { 126 | if (options.percentage) { 127 | var value = fixFloatError(100.0 / this.decimalValue); 128 | 129 | // HACK: Oddslib.prototype.to calls decimalAdjust if we return a number. 130 | // But we need to round before adding the % symbol. 131 | // So we do it here and return the string 132 | if (options.precision !== null) { 133 | value = decimalAdjust("round", value, -options.precision); 134 | } 135 | 136 | return value.toString() + "%"; 137 | } 138 | 139 | return fixFloatError(1 / this.decimalValue); 140 | }, 141 | }, 142 | 143 | // UK/Fractional format 144 | fractional: { 145 | from: function (n) { 146 | // Try to split on the slash 147 | var pieces = n.toString().split("/"); 148 | 149 | n = parseFloat(pieces[0]); 150 | 151 | var d; 152 | if (pieces.length === 2) { 153 | d = parseFloat(pieces[1]); 154 | } else if (pieces.length === 1) { 155 | d = 1; 156 | } else { 157 | throw new Error("Invalid fraction"); 158 | } 159 | 160 | if (n === 0 || d === 0 || n / d <= 0.0) { 161 | throw new Error("Outside valid range"); 162 | } 163 | 164 | return 1 + n / d; 165 | }, 166 | to: function (options) { 167 | return approximateFraction( 168 | this.decimalValue - 1, 169 | options.precision || 12 170 | ); 171 | }, 172 | }, 173 | 174 | // Malay format 175 | malay: { 176 | from: function (malay) { 177 | malay = parseFloat(malay); 178 | 179 | if (malay <= -1.0 || malay > 1.0) { 180 | throw new Error("Outside valid range."); 181 | } 182 | 183 | if (malay < 0) { 184 | malay = -1 / malay; 185 | } 186 | return malay + 1; 187 | }, 188 | to: function () { 189 | if (this.decimalValue <= 2.0) { 190 | return fixFloatError(this.decimalValue - 1); 191 | } 192 | return fixFloatError(-1 / (this.decimalValue - 1)); 193 | }, 194 | }, 195 | 196 | // Indonesian format 197 | indonesian: { 198 | from: function (indonesian) { 199 | indonesian = parseFloat(indonesian); 200 | 201 | if (indonesian === 0) { 202 | throw new Error("Outside valid range."); 203 | } 204 | 205 | if (indonesian >= 1) { 206 | return indonesian + 1; 207 | } 208 | return -1 / indonesian + 1; 209 | }, 210 | to: function () { 211 | if (this.decimalValue < 2.0) { 212 | return fixFloatError(-1 / (this.decimalValue - 1)); 213 | } 214 | return fixFloatError(this.decimalValue - 1); 215 | }, 216 | }, 217 | }; 218 | 219 | export const Odds = (function () { 220 | // Private constructor pattern 221 | // from http://stackoverflow.com/a/21731713 222 | var PublicOdds = function () { 223 | throw new Error( 224 | "This constructor is private, please use the from* functions" 225 | ); 226 | }; 227 | var Odds = function (decimalValue) { 228 | if (typeof decimalValue !== "number" || isNaN(decimalValue)) { 229 | throw new Error("Invalid odds"); 230 | } 231 | 232 | this.decimalValue = fixFloatError(decimalValue); 233 | }; 234 | Odds.prototype = PublicOdds.prototype; 235 | 236 | // Generic constructor 237 | PublicOdds.from = function (format, value) { 238 | if (!FORMATS.hasOwnProperty(format)) { 239 | throw new Error("Unknown format " + format + "."); 240 | } 241 | var decimal = FORMATS[format].from(value); 242 | return new Odds(decimal); 243 | }; 244 | 245 | return PublicOdds; 246 | })(); 247 | 248 | // Conversion API 249 | Odds.prototype.to = function (format, options) { 250 | if (!FORMATS.hasOwnProperty(format)) { 251 | throw new Error("Unknown format " + format + "."); 252 | } 253 | 254 | options = Object.assign( 255 | { 256 | precision: null, 257 | percentage: false, 258 | }, 259 | options 260 | ); 261 | 262 | var ret = FORMATS[format].to.call(this, options); 263 | if (typeof ret === "number" && options.precision !== null) { 264 | ret = decimalAdjust("round", ret, -options.precision); 265 | } 266 | return ret; 267 | }; 268 | 269 | export const from = Odds.from; 270 | -------------------------------------------------------------------------------- /test/index.mjs: -------------------------------------------------------------------------------- 1 | import chai from "chai"; 2 | 3 | let should = chai.should(), 4 | expect = chai.expect; 5 | 6 | import * as oddslib from "../oddslib.mjs"; 7 | 8 | describe("decimal odds", function () { 9 | it("can be constructed", function () { 10 | oddslib.from("decimal", 1.5).should.be.an.instanceof(oddslib.Odds); 11 | oddslib.from("decimal", 1.5).to("decimal").should.equal(1.5); 12 | oddslib.from("decimal", "1.5").to("decimal").should.equal(1.5); 13 | }); 14 | 15 | it("can be converted to", function () { 16 | oddslib.from("decimal", 1.5).to("decimal").should.equal(1.5); 17 | }); 18 | 19 | it("throws an error when outside range", function () { 20 | // valid range: x > 1.0 21 | oddslib.from("decimal", 1.01).should.be.an.instanceof(oddslib.Odds); 22 | oddslib.from("decimal", 10.0).should.be.an.instanceof(oddslib.Odds); 23 | oddslib.from("decimal", 100000.0).should.be.an.instanceof(oddslib.Odds); 24 | 25 | expect(function () { 26 | oddslib.from("decimal", 1.0); 27 | }).to.throw(Error); 28 | expect(function () { 29 | oddslib.from("decimal", 0.98); 30 | }).to.throw(Error); 31 | expect(function () { 32 | oddslib.from("decimal", 0); 33 | }).to.throw(Error); 34 | expect(function () { 35 | oddslib.from("decimal", -0.5); 36 | }).to.throw(Error); 37 | }); 38 | 39 | it("throws an error when invalid odds are passed", function () { 40 | expect(function () { 41 | oddslib.from("decimal", "foo"); 42 | }).to.throw(Error); 43 | expect(function () { 44 | oddslib.from("decimal", "-"); 45 | }).to.throw(Error); 46 | }); 47 | 48 | it("support custom precision", function () { 49 | oddslib 50 | .from("decimal", 1.005) 51 | .to("decimal", { precision: 2 }) 52 | .should.equal(1.01); 53 | oddslib 54 | .from("decimal", 1.005) 55 | .to("decimal", { precision: 0 }) 56 | .should.equal(1); 57 | }); 58 | }); 59 | 60 | describe("Moneyline odds", function () { 61 | it("can be constructed", function () { 62 | oddslib.from("moneyline", -500).should.be.an.instanceof(oddslib.Odds); 63 | oddslib.from("moneyline", -500).to("decimal").should.equal(1.2); 64 | oddslib.from("moneyline", "-500").to("decimal").should.equal(1.2); 65 | oddslib.from("moneyline", 500).to("decimal").should.equal(6.0); 66 | oddslib.from("moneyline", 0).to("decimal").should.equal(1.0); 67 | }); 68 | 69 | it("can be converted to", function () { 70 | oddslib.from("decimal", 1.8).to("moneyline").should.equal(-125); 71 | oddslib.from("decimal", 4.5).to("moneyline").should.equal(350); 72 | }); 73 | 74 | it("throws an error when invalid odds are passed", function () { 75 | expect(function () { 76 | oddslib.from("moneyline", "foo"); 77 | }).to.throw(Error); 78 | expect(function () { 79 | oddslib.from("moneyline", "-"); 80 | }).to.throw(Error); 81 | }); 82 | 83 | it("support custom precision", function () { 84 | oddslib 85 | .from("moneyline", -499.999) 86 | .to("moneyline", { precision: 2 }) 87 | .should.equal(-500); 88 | }); 89 | }); 90 | 91 | describe("Hong Kong odds", function () { 92 | it("can be constructed", function () { 93 | oddslib.from("hongKong", 0.2).should.be.an.instanceof(oddslib.Odds); 94 | oddslib.from("hongKong", 0.2).to("decimal").should.equal(1.2); 95 | oddslib.from("hongKong", "0.2").to("decimal").should.equal(1.2); 96 | }); 97 | 98 | it("can be converted to", function () { 99 | oddslib.from("decimal", 1.2).to("hongKong").should.equal(0.2); 100 | oddslib.from("decimal", 11.76).to("hongKong").should.equal(10.76); 101 | }); 102 | 103 | it("throws an error when outside range", function () { 104 | // valid range: x >= 0.0 105 | oddslib.from("hongKong", 0.0).should.be.an.instanceof(oddslib.Odds); 106 | oddslib.from("hongKong", 1.0).should.be.an.instanceof(oddslib.Odds); 107 | oddslib.from("hongKong", 10.0).should.be.an.instanceof(oddslib.Odds); 108 | oddslib.from("hongKong", 100000.0).should.be.an.instanceof(oddslib.Odds); 109 | 110 | expect(function () { 111 | oddslib.from("hongKong", -0.5); 112 | }).to.throw(Error); 113 | }); 114 | 115 | it("throws an error when invalid odds are passed", function () { 116 | expect(function () { 117 | oddslib.from("hongKong", "foo"); 118 | }).to.throw(Error); 119 | expect(function () { 120 | oddslib.from("hongKong", "-"); 121 | }).to.throw(Error); 122 | }); 123 | 124 | it("support custom precision", function () { 125 | oddslib 126 | .from("hongKong", 1.023) 127 | .to("hongKong", { precision: 2 }) 128 | .should.equal(1.02); 129 | }); 130 | }); 131 | 132 | describe("Fractional odds", function () { 133 | it("can be constructed", function () { 134 | oddslib.from("fractional", "5/2").to("decimal").should.equal(3.5); 135 | oddslib 136 | .from("fractional", 5 / 2) 137 | .to("decimal") 138 | .should.equal(3.5); 139 | oddslib.from("fractional", 2.5).to("decimal").should.equal(3.5); 140 | 141 | expect(function () { 142 | oddslib.from("fractional", "5/2/3"); 143 | }).to.throw(Error); 144 | }); 145 | 146 | it("can be converted to", function () { 147 | oddslib.from("decimal", 1.4).to("fractional").should.equal("2/5"); 148 | oddslib.from("decimal", 3.5).to("fractional").should.equal("5/2"); 149 | oddslib.from("decimal", 2).to("fractional").should.equal("1/1"); 150 | }); 151 | 152 | it("throws an error when outside range", function () { 153 | // valid range: x > 0 and denominator != 0 154 | oddslib.from("fractional", 0.1).should.be.an.instanceof(oddslib.Odds); 155 | oddslib.from("fractional", "0.1/3").should.be.an.instanceof(oddslib.Odds); 156 | oddslib.from("fractional", "3/1").should.be.an.instanceof(oddslib.Odds); 157 | 158 | expect(function () { 159 | oddslib.from("fractional", 0); 160 | }).to.throw(Error); 161 | expect(function () { 162 | oddslib.from("fractional", "0/0"); 163 | }).to.throw(Error); 164 | expect(function () { 165 | oddslib.from("fractional", "0/3"); 166 | }).to.throw(Error); 167 | expect(function () { 168 | oddslib.from("fractional", "3/0"); 169 | }).to.throw(Error); 170 | expect(function () { 171 | oddslib.from("fractional", "-5/2"); 172 | }).to.throw(Error); 173 | expect(function () { 174 | oddslib.from("fractional", "2/-5"); 175 | }).to.throw(Error); 176 | }); 177 | 178 | it("throws an error when invalid odds are passed", function () { 179 | expect(function () { 180 | oddslib.from("fractional", "foo"); 181 | }).to.throw(Error); 182 | expect(function () { 183 | oddslib.from("fractional", "5/"); 184 | }).to.throw(Error); 185 | expect(function () { 186 | oddslib.from("fractional", "5//2"); 187 | }).to.throw(Error); 188 | expect(function () { 189 | oddslib.from("fractional", "-"); 190 | }).to.throw(Error); 191 | }); 192 | 193 | it("support custom precision", function () { 194 | oddslib.from("decimal", 2.023).to("fractional").should.equal("1023/1000"); 195 | oddslib 196 | .from("decimal", 2.023) 197 | .to("fractional", { precision: 2 }) 198 | .should.equal("44/43"); 199 | }); 200 | }); 201 | 202 | describe("Implied probability", function () { 203 | it("can be constructed", function () { 204 | oddslib 205 | .from("impliedProbability", 0.5) 206 | .should.be.an.instanceof(oddslib.Odds); 207 | oddslib.from("impliedProbability", 0.5).to("decimal").should.equal(2.0); 208 | oddslib.from("impliedProbability", "0.5").to("decimal").should.equal(2.0); 209 | }); 210 | 211 | it("accepts percentage strings", function () { 212 | oddslib 213 | .from("impliedProbability", "10%") 214 | .to("impliedProbability") 215 | .should.equal(0.1); 216 | }); 217 | 218 | it("can be converted to", function () { 219 | oddslib.from("decimal", 1.6).to("impliedProbability").should.equal(0.625); 220 | oddslib.from("decimal", 2.5).to("impliedProbability").should.equal(0.4); 221 | }); 222 | 223 | it("throws an error when outside range", function () { 224 | // valid range: 0 < x < 1.0 225 | oddslib 226 | .from("impliedProbability", 0.1) 227 | .should.be.an.instanceof(oddslib.Odds); 228 | oddslib 229 | .from("impliedProbability", 0.5) 230 | .should.be.an.instanceof(oddslib.Odds); 231 | oddslib 232 | .from("impliedProbability", 0.99) 233 | .should.be.an.instanceof(oddslib.Odds); 234 | 235 | expect(function () { 236 | oddslib.from("impliedProbability", -1.0); 237 | }).to.throw(Error); 238 | expect(function () { 239 | oddslib.from("impliedProbability", 0.0); 240 | }).to.throw(Error); 241 | expect(function () { 242 | oddslib.from("impliedProbability", 1.0); 243 | }).to.throw(Error); 244 | }); 245 | 246 | it("throws an error when invalid odds are passed", function () { 247 | expect(function () { 248 | oddslib.from("impliedProbability", "foo"); 249 | }).to.throw(Error); 250 | expect(function () { 251 | oddslib.from("impliedProbability", "-"); 252 | }).to.throw(Error); 253 | }); 254 | 255 | it("supports custom precision", function () { 256 | oddslib 257 | .from("impliedProbability", 0.666) 258 | .to("impliedProbability", { precision: 2 }) 259 | .should.equal(0.67); 260 | }); 261 | 262 | it("can be formatted as a percentage", function () { 263 | oddslib 264 | .from("impliedProbability", 0.5) 265 | .to("impliedProbability", { percentage: true }) 266 | .should.equal("50%"); 267 | 268 | // custom precision percentage 269 | oddslib 270 | .from("impliedProbability", 0.6666) 271 | .to("impliedProbability", { 272 | precision: 1, 273 | percentage: true, 274 | }) 275 | .should.equal("66.7%"); 276 | }); 277 | }); 278 | 279 | describe("Malay odds", function () { 280 | it("can be constructed", function () { 281 | oddslib.from("malay", 0.2).should.be.an.instanceof(oddslib.Odds); 282 | oddslib.from("malay", -0.4).to("decimal").should.equal(3.5); 283 | oddslib.from("malay", "-0.4").to("decimal").should.equal(3.5); 284 | oddslib.from("malay", 0.75).to("decimal").should.equal(1.75); 285 | }); 286 | 287 | it("can be converted to", function () { 288 | oddslib.from("decimal", 1.1).to("malay").should.equal(0.1); 289 | oddslib.from("decimal", 2.0).to("malay").should.equal(1.0); 290 | oddslib.from("moneyline", 400).to("malay").should.equal(-0.25); 291 | }); 292 | 293 | it("throws an error when outside range", function () { 294 | // valid range: -1 < x <= 1.0 295 | oddslib.from("malay", -0.99).should.be.an.instanceof(oddslib.Odds); 296 | oddslib.from("malay", 0).should.be.an.instanceof(oddslib.Odds); 297 | oddslib.from("malay", 0.5).should.be.an.instanceof(oddslib.Odds); 298 | oddslib.from("malay", 1.0).should.be.an.instanceof(oddslib.Odds); 299 | 300 | expect(function () { 301 | oddslib.from("malay", -1.0); 302 | }).to.throw(Error); 303 | expect(function () { 304 | oddslib.from("malay", 1.01); 305 | }).to.throw(Error); 306 | }); 307 | 308 | it("throws an error when invalid odds are passed", function () { 309 | expect(function () { 310 | oddslib.from("malay", "foo"); 311 | }).to.throw(Error); 312 | expect(function () { 313 | oddslib.from("malay", "-"); 314 | }).to.throw(Error); 315 | }); 316 | 317 | it("supports custom precision", function () { 318 | oddslib 319 | .from("malay", -0.8999) 320 | .to("malay", { precision: 1 }) 321 | .should.equal(-0.9); 322 | }); 323 | }); 324 | 325 | describe("Indonesian odds", function () { 326 | it("can be constructed", function () { 327 | oddslib.from("indonesian", -5.0).should.be.an.instanceof(oddslib.Odds); 328 | oddslib.from("indonesian", -5.0).to("decimal").should.equal(1.2); 329 | oddslib.from("indonesian", 3.0).to("decimal").should.equal(4.0); 330 | oddslib.from("indonesian", "3.0").to("decimal").should.equal(4.0); 331 | }); 332 | 333 | it("can be converted to", function () { 334 | oddslib.from("decimal", 1.1).to("indonesian").should.equal(-10.0); 335 | oddslib.from("decimal", 2.0).to("indonesian").should.equal(1.0); 336 | oddslib.from("moneyline", 400).to("indonesian").should.equal(4.0); 337 | }); 338 | 339 | it("throws an error when outside range", function () { 340 | // valid range: x != 0 341 | oddslib.from("indonesian", -5).should.be.an.instanceof(oddslib.Odds); 342 | oddslib.from("indonesian", -0.01).should.be.an.instanceof(oddslib.Odds); 343 | oddslib.from("indonesian", 0.01).should.be.an.instanceof(oddslib.Odds); 344 | oddslib.from("indonesian", 1.0).should.be.an.instanceof(oddslib.Odds); 345 | oddslib.from("indonesian", 5.0).should.be.an.instanceof(oddslib.Odds); 346 | 347 | expect(function () { 348 | oddslib.from("indonesian", 0.0); 349 | }).to.throw(Error); 350 | }); 351 | 352 | it("throws an error when invalid odds are passed", function () { 353 | expect(function () { 354 | oddslib.from("indonesian", "foo"); 355 | }).to.throw(Error); 356 | expect(function () { 357 | oddslib.from("indonesian", "-"); 358 | }).to.throw(Error); 359 | }); 360 | 361 | it("support custom precision", function () { 362 | oddslib 363 | .from("indonesian", -50.678) 364 | .to("indonesian", { precision: 1 }) 365 | .should.equal(-50.7); 366 | }); 367 | }); 368 | 369 | describe("odds construction", function () { 370 | it("cannot be called manually", function () { 371 | expect(oddslib.Odds).to.throw(Error); 372 | }); 373 | }); 374 | --------------------------------------------------------------------------------