├── .travis.yml ├── .gitignore ├── example.js ├── package.json ├── lib ├── core.js ├── reverse-pairs.js └── reverse-non-pairs.js ├── LICENSE ├── prange.reverse.js ├── README.md ├── test ├── reverse.js └── prange.js └── prange.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const prange = require('./') 4 | 5 | const r1 = prange('AKs-ATs, QQ+') 6 | const r2 = prange('JTs-54s') 7 | 8 | console.log(r1) 9 | // [ 'AA', 'AKs', 'AQs', 'AJs', 'ATs', 'KK', 'QQ' ] 10 | 11 | console.log(r2) 12 | // [ 'JTs', 'T9s', '98s', '87s', '76s', '65s', '54s' ] 13 | 14 | console.log(prange.reverse(r1)) 15 | // QQ+, ATs+ 16 | 17 | console.log(prange.reverse(r2)) 18 | // JTs-54s 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prange", 3 | "version": "0.2.3", 4 | "description": "Parses poker hand range short descriptions into a range array.", 5 | "main": "prange.js", 6 | "scripts": { 7 | "test": "set -e; for t in test/*.js; do node $t; done" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/thlorenz/prange.git" 12 | }, 13 | "homepage": "https://github.com/thlorenz/prange", 14 | "dependencies": {}, 15 | "devDependencies": { 16 | "tape": "~4.6.3" 17 | }, 18 | "keywords": [ 19 | "poker", 20 | "range", 21 | "parser" 22 | ], 23 | "author": { 24 | "name": "Thorsten Lorenz", 25 | "email": "thlorenz@gmx.de", 26 | "url": "http://thlorenz.com" 27 | }, 28 | "license": { 29 | "type": "MIT", 30 | "url": "https://github.com/thlorenz/prange/blob/master/LICENSE" 31 | }, 32 | "engine": { 33 | "node": ">=6" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/core.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rankCodes = { 4 | '2': 0 5 | , '3': 1 6 | , '4': 2 7 | , '5': 3 8 | , '6': 4 9 | , '7': 5 10 | , '8': 6 11 | , '9': 7 12 | , 'T': 8 13 | , 'J': 9 14 | , 'Q': 10 15 | , 'K': 11 16 | , 'A': 12 17 | } 18 | 19 | const codeRanks = { 20 | 0 : '2' 21 | , 1 : '3' 22 | , 2 : '4' 23 | , 3 : '5' 24 | , 4 : '6' 25 | , 5 : '7' 26 | , 6 : '8' 27 | , 7 : '9' 28 | , 8 : 'T' 29 | , 9 : 'J' 30 | , 10 : 'Q' 31 | , 11 : 'K' 32 | , 12 : 'A' 33 | } 34 | 35 | function byCodeRankDescending(a, b) { 36 | const codea1 = rankCodes[a[0]] 37 | const codeb1 = rankCodes[b[0]] 38 | if (codea1 < codeb1) return 1 39 | if (codeb1 < codea1) return -1 40 | 41 | const codea2 = rankCodes[a[1]] 42 | const codeb2 = rankCodes[b[1]] 43 | return codea2 < codeb2 ? 1 : -1 44 | } 45 | 46 | module.exports = { rankCodes, codeRanks, byCodeRankDescending } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Thorsten Lorenz. 2 | All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /lib/reverse-pairs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { rankCodes } = require('./core') 4 | 5 | function groupify(pairs) { 6 | const containedCodes = [ null, null, null, null, 7 | null, null, null, null, 8 | null, null, null, null, null ] 9 | 10 | for (const p of pairs) { 11 | const code = rankCodes[p[0]] 12 | containedCodes[code] = p 13 | } 14 | 15 | const reversed = containedCodes.reverse() 16 | const groups = [] 17 | let group = [] 18 | for (let i = 0; i < reversed.length; i++) { 19 | const val = reversed[i] 20 | if (val == null) { 21 | if (group.length) groups.push(group) 22 | group = [] 23 | continue 24 | } 25 | group.push(val) 26 | } 27 | if (group.length) groups.push(group) 28 | return groups 29 | } 30 | 31 | function toShortNotation(group) { 32 | const first = group[0] 33 | if (group.length === 1) return first 34 | const last = group[group.length - 1] 35 | return group[0] === 'AA' ? last + '+' : first + '-' + last 36 | } 37 | 38 | module.exports = function reversePairs(pairs) { 39 | const groups = groupify(pairs) 40 | return groups.map(toShortNotation) 41 | } 42 | -------------------------------------------------------------------------------- /prange.reverse.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const reversePairs = require('./lib/reverse-pairs') 4 | const reverseNonPairs = require('./lib/reverse-non-pairs') 5 | 6 | function sortOut(combos) { 7 | const offsuit = new Set() 8 | const suited = new Set() 9 | const pairs = new Set() 10 | for (let i = 0; i < combos.length; i++) { 11 | const [ rank1, rank2, suit ] = combos[i].trim() 12 | const desc = rank1 + rank2 13 | if (rank1 === rank2) { 14 | pairs.add(desc) 15 | continue 16 | } 17 | if (suit == null || suit === 'o') { 18 | offsuit.add(desc) 19 | continue 20 | } 21 | if (suit === 's') { 22 | suited.add(desc) 23 | continue 24 | } 25 | throw new Error('Invalid suit "' + suit + '" of "' + combos[i] + '"!') 26 | } 27 | 28 | return { offsuit, suited, pairs } 29 | } 30 | 31 | function unsuitWhenPossible(os, su) { 32 | const consolidated = [] 33 | for (const n of os) { 34 | const suitedVersion = n.replace(/o/g, 's') 35 | if (su.has(suitedVersion)) { 36 | consolidated.push(n.replace(/o/g, '')) 37 | su.delete(suitedVersion) 38 | } else { 39 | consolidated.push(n) 40 | } 41 | } 42 | return consolidated.concat(Array.from(su)) 43 | } 44 | 45 | /** 46 | * Converts a poker hand range to short notation. 47 | * It's the opposite of `prange`. 48 | * 49 | * @name prange.reverse 50 | * @function 51 | * @param {Array.} combos hand combos to be converted to short notation 52 | * @param {String} the short notation for the range 53 | */ 54 | function reverse(combos) { 55 | const { offsuit, suited, pairs } = sortOut(combos) 56 | 57 | const ps = reversePairs(pairs) 58 | const os = reverseNonPairs(offsuit, 'o') 59 | const su = reverseNonPairs(suited, 's') 60 | const nonpairs = unsuitWhenPossible(new Set(os), new Set(su)) 61 | 62 | return ps.concat(nonpairs).join(', ') 63 | } 64 | 65 | module.exports = reverse 66 | module.exports.sortOut = sortOut 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prange [![build status](https://secure.travis-ci.org/thlorenz/prange.png)](http://travis-ci.org/thlorenz/prange) 2 | 3 | Parses poker hand range short notation into a range array. 4 | 5 | ```js 6 | const prange = require('./') 7 | 8 | const r1 = prange('AKs-ATs, QQ+') 9 | const r2 = prange('JTs-54s') 10 | 11 | console.log(r1) 12 | // [ 'AA', 'AKs', 'AQs', 'AJs', 'ATs', 'KK', 'QQ' ] 13 | 14 | console.log(r2) 15 | // [ 'JTs', 'T9s', '98s', '87s', '76s', '65s', '54s' ] 16 | 17 | console.log(prange.reverse(r1)) 18 | // QQ+, ATs+ 19 | 20 | console.log(prange.reverse(r2)) 21 | // JTs-54s 22 | ``` 23 | 24 | ## Installation 25 | 26 | npm install prange 27 | 28 | ## [API](https://thlorenz.github.io/prange) 29 | 30 | 31 | 32 | ### prange 33 | 34 | Converts a short notation for poker hand ranges into an array 35 | filled with the matching combos. 36 | 37 | Each range specifier is separated by a comma. 38 | 39 | The following notations are supported: 40 | 41 | - single combos `KK, AK, ATs` 42 | - plus notation 43 | - `QQ+` = `[ AA, KK, QQ ]` 44 | - `KTs+` = `[ KQs, KJs, KTs ]` 45 | - `KTo+` = `[ KQo, KJo, KTo ]` 46 | - `KT+` = `[ KQs, KQo, KJo, KJs, KTo, KTs ]` 47 | - dash notation 48 | - `KK-JJ` = `[ KK, QQ, JJ ]` 49 | - `AKo-ATo` = `[ AK, AQ, AJ, AT ]` 50 | - `AKs-JTs` = `[ AKs, KQs, JTs ]` 51 | 52 | **Parameters** 53 | 54 | - `s` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** the short notation for the range 55 | 56 | Returns **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** all hand combos satisfying the given range 57 | 58 | ### prange.reverse 59 | 60 | Converts a poker hand range to short notation. 61 | It's the opposite of `prange`. 62 | 63 | **Parameters** 64 | 65 | - `combos` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** hand combos to be converted to short notation 66 | - `the` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** short notation for the range 67 | 68 | ## License 69 | 70 | MIT 71 | -------------------------------------------------------------------------------- /test/reverse.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const { reverse } = require('../') 5 | 6 | test('\nreversing pairs', function(t) { 7 | [ [ [ 'AA', 'KK', 'QQ', 'JJ', 'TT', '88', '55', '44', '33', '22' ] 8 | , 'TT+, 88, 55-22' ] 9 | , [ [ 'AA', 'KK', 'QQ', 'JJ', 'TT', '99', '88', '77', '66', '55', '44', '33', '22' ] 10 | , '22+' ] 11 | , [ [ 'AA', 'KK', 'QQ', 'TT', '99', '77', '55', '44', '33' ] 12 | , 'QQ+, TT-99, 77, 55-33' ] 13 | , [ [ 'KK' ], 'KK' ] 14 | ].forEach(check) 15 | 16 | function check([ pairs, reversed ]) { 17 | const s = reverse(pairs) 18 | t.equal(s, reversed, 19 | `${pairs} reverses to ${reversed}` 20 | ) 21 | } 22 | t.end() 23 | }) 24 | 25 | test('\nnon pairs first rank same, increasing second rank', function(t) { 26 | [ [ [ 'AKo', 'AKs', 'AQo', 'AQs', 'AJo', 'AJs', 'ATo', 'ATs', 'KQo', 'KQs' ] 27 | , 'AT+, KQ' ] 28 | , [ [ 'AKo', 'AQo', 'AQs', 'AJo', 'AJs', 'ATo', 'ATs', 'KQo', 'KQs' ] 29 | , 'ATo+, KQ, AQs-ATs' ] 30 | , [ [ 'AKo', 'AKs', 'AQo', 'AQs', 'AJo', 'AJs', 'ATo', 'ATs', 'KQo', 'KQs', 'KJs', 'KTs', 'K9s', 'K8s' ] 31 | , 'AT+, KQo, K8s+' ] 32 | ].forEach(check) 33 | 34 | function check([ nonpairs, reversed ]) { 35 | const s = reverse(nonpairs) 36 | t.equal(s, reversed, 37 | `${nonpairs} reverses to ${reversed}` 38 | ) 39 | } 40 | t.end() 41 | }) 42 | 43 | test('\nsuited connectors', function(t) { 44 | [ [ [ 'JTs', 'T9s', '98s', '87s', '76s', '65s', '54s' ] 45 | , 'JTs-54s' ] 46 | , [ [ 'JTs', 'T9s', '98s', '76s', '65s', '54s' ] 47 | , 'JTs-98s, 76s-54s' ] 48 | , [ [ 'JTs', 'J9s', 'J8s', 'JTo', 'J9o', 'J8o', 'T9s', '98s', '87s', '76s', '65s', '54s' ] 49 | , 'J8+, T9s-54s' ] 50 | , [ [ 'JTs', 'T9s', '98s', '87s' ], 'JTs-87s' ] 51 | , [ [ 'AKs', 'KQs', 'QJs' ], 'AKs-QJs' ] 52 | ].forEach(check) 53 | 54 | function check([ nonpairs, reversed ]) { 55 | const s = reverse(nonpairs) 56 | t.equal(s, reversed, 57 | `${nonpairs} reverses to ${reversed}` 58 | ) 59 | } 60 | t.end() 61 | }) 62 | 63 | test('\ngappers', function(t) { 64 | [ [ [ 'J9o', 'T8o', '97os', '86o' ] 65 | , 'J9o-86o' ] 66 | , [ [ 'JTo', 'J9o', 'J8o', 'T8o', '97os', '86o' ] 67 | , 'J8o+, T8o-86o' ] 68 | ].forEach(check) 69 | 70 | function check([ nonpairs, reversed ]) { 71 | const s = reverse(nonpairs) 72 | t.equal(s, reversed, 73 | `${nonpairs} reverses to ${reversed}` 74 | ) 75 | } 76 | t.end() 77 | }) 78 | -------------------------------------------------------------------------------- /test/prange.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const prange = require('../') 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | function inspect(obj, depth) { 8 | console.error(require('util').inspect(obj, false, depth || 5, true)) 9 | } 10 | 11 | test('\ndash notation', function(t) { 12 | let s = 'AA-TT, 22-55, 88' 13 | t.deepEqual(prange(s), 14 | [ 'AA', 'KK', 'QQ', 'JJ', 'TT', '88', '55', '44', '33', '22' ] 15 | , 'pairs only: ' + s 16 | ) 17 | s = 'AKo-ATo, KQo-KJ' 18 | t.deepEqual(prange(s), 19 | [ 'AKo', 'AQo', 'AJo', 'ATo', 'KQo', 'KJo' ] 20 | , 'non-pairs offsuit: ' + s 21 | ) 22 | s = 'AKs-AT, JTs-J8s, 54-53s' 23 | t.deepEqual(prange(s), 24 | [ 'AKs', 'AQs', 'AJs', 'ATs', 'JTs', 'J9s', 'J8s', '54s', '53s' ] 25 | , 'non-pairs suited: ' + s 26 | ) 27 | s = 'AK-AT, KQ-KJ' 28 | t.deepEqual(prange(s), 29 | [ 'AKo', 'AKs', 'AQo', 'AQs', 'AJo', 'AJs', 'ATs', 'ATo', 'KQo', 'KQs', 'KJo', 'KJs' ] 30 | , 'non-pairs no suit given: ' + s 31 | ) 32 | s = 'AK-JT, 97s-53s' 33 | t.deepEqual(prange(s), 34 | [ 'AKo', 'AKs', 'KQo', 'KQs', 'QJo', 'QJs', 'JTs', 'JTo', 35 | '97s', '86s', '75s', '64s', '53s' ] 36 | , 'connectors and gappers: ' + s 37 | ) 38 | t.end() 39 | }) 40 | 41 | test('\nplus notation', function(t) { 42 | let s = 'QQ+' 43 | t.deepEqual(prange(s), 44 | [ 'AA', 'KK', 'QQ' ] 45 | , s 46 | ) 47 | s = '44+' 48 | t.deepEqual(prange(s), 49 | [ 'AA', 'KK', 'QQ', 'JJ', 'TT', '99', '88', '77', '66', '55', '44' ] 50 | , s 51 | ) 52 | s = 'KK+, 77' 53 | t.deepEqual(prange(s), 54 | [ 'AA', 'KK', '77' ] 55 | , s 56 | ) 57 | s = 'AT+, KQ+' 58 | t.deepEqual(prange(s), 59 | [ 'AKo', 'AKs', 'AQo', 'AQs', 'AJo', 'AJs', 'ATo', 'ATs', 'KQo', 'KQs' ] 60 | , s 61 | ) 62 | 63 | s = 'A2s+, QTs+, AJo+' 64 | t.deepEqual(prange(s), 65 | [ 'AKo', 'AKs', 'AQo', 'AQs', 'AJo', 66 | 'AJs', 'ATs', 'A9s', 'A8s', 'A7s', 67 | 'A6s', 'A5s', 'A4s', 'A3s', 'A2s', 68 | 'QJs', 'QTs' ] 69 | , s 70 | ) 71 | t.end() 72 | }) 73 | 74 | test('\nsingle combos', function(t) { 75 | let s = 'AKo, AQs, AJ' 76 | t.deepEqual(prange(s), [ 'AKo', 'AQs', 'AJo', 'AJs' ], s) 77 | 78 | t.end() 79 | }) 80 | 81 | test('\nseparation by spaces and error correction', function(t) { 82 | let s = 'AKo AQs AJ' 83 | t.deepEqual(prange(s), [ 'AKo', 'AQs', 'AJo', 'AJs' ], s) 84 | s = 'AKo AQs AJ' 85 | t.deepEqual(prange(s), [ 'AKo', 'AQs', 'AJo', 'AJs' ], s) 86 | s = 'AKo AQs , AJ' 87 | t.deepEqual(prange(s), [ 'AKo', 'AQs', 'AJo', 'AJs' ], s) 88 | 89 | s = 'AKs - ATs AJ' 90 | t.deepEqual(prange(s), [ 'AKs', 'AQs', 'AJs', 'AJo', 'ATs' ], s) 91 | 92 | s = 'AKs - AQs, AJs - ATs AJ' 93 | t.deepEqual(prange(s), [ 'AKs', 'AQs', 'AJs', 'AJo', 'ATs' ], s) 94 | 95 | s = 'AJs + ,AT AJ' 96 | t.deepEqual(prange(s), [ 'AKs', 'AQs', 'AJs', 'AJo', 'ATo', 'ATs' ], s) 97 | t.end() 98 | }) 99 | -------------------------------------------------------------------------------- /lib/reverse-non-pairs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // 4 | // None of the code here is elegant as I prefered to spell things out by hand 5 | // instead of having to overheat my brain. 6 | // If you find a more elegant solution, granted I can still follow it and it's 7 | // not overly elegant, please submit a PR :) 8 | // 9 | 10 | function toHash(highRank, arr) { 11 | const hash = {} 12 | for (let i = 0; i < arr.length; i++) { 13 | hash[highRank + arr[i]] = i 14 | } 15 | hash._size = arr.length 16 | return hash 17 | } 18 | 19 | const connectors = { 20 | '32': 11 21 | , '43': 10 22 | , '54': 9 23 | , '65': 8 24 | , '76': 7 25 | , '87': 6 26 | , '98': 5 27 | , 'T9': 4 28 | , 'JT': 3 29 | , 'QJ': 2 30 | , 'KQ': 1 31 | , 'AK': 0 32 | , _size: 12 33 | } 34 | 35 | const gappers = { 36 | '42': 10 37 | , '53': 9 38 | , '64': 8 39 | , '75': 7 40 | , '86': 6 41 | , '97': 5 42 | , 'T8': 4 43 | , 'J9': 3 44 | , 'QT': 2 45 | , 'KJ': 1 46 | , 'AQ': 0 47 | , _size: 11 48 | } 49 | 50 | const ranks = [ '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A' ].reverse() 51 | const axArray = ranks.slice(1) 52 | const kxArray = axArray.slice(1) 53 | const qxArray = kxArray.slice(1) 54 | const jxArray = qxArray.slice(1) 55 | const txArray = jxArray.slice(1) 56 | const _9xArray = txArray.slice(1) 57 | const _8xArray = _9xArray.slice(1) 58 | 59 | const ax = toHash('A', axArray) 60 | const kx = toHash('K', kxArray) 61 | const qx = toHash('Q', qxArray) 62 | const jx = toHash('J', jxArray) 63 | const tx = toHash('T', txArray) 64 | const _9x = toHash('9', _9xArray) 65 | const _8x = toHash('8', _8xArray) 66 | 67 | // Detect connections in the following order: 68 | // Ax+, Kx+, Qx+, Jx+, connectors, gappers, Tx+, 9x+, 8x+ 69 | // Only accept all groups that are at least 3 long 70 | const orderedHashes = [ ax, kx, qx, jx, connectors, gappers, tx, _9x, _8x ] 71 | 72 | function processCombos(hash, combos) { 73 | const slots = new Array(hash._size) 74 | const keys = Object.keys(hash) 75 | 76 | function updateSlot(k) { 77 | const idx = hash[k] 78 | const val = combos.has(k) ? k : null 79 | slots[idx] = val 80 | } 81 | keys.forEach(updateSlot) 82 | 83 | const groups = [] 84 | let group = [] 85 | for (let i = 0; i < slots.length; i++) { 86 | const val = slots[i] 87 | if (val == null) { 88 | if (group.length) groups.push(group) 89 | group = [] 90 | continue 91 | } 92 | group.push(val) 93 | } 94 | if (group.length) groups.push(group) 95 | return groups 96 | } 97 | 98 | function processGroups(groups, remainingCombos) { 99 | // don't allow smaller groups than 3 100 | const processedGroups = [] 101 | for (let i = 0; i < groups.length; i++) { 102 | const combos = groups[i] 103 | if (combos.length < 3) continue 104 | processedGroups.push(combos) 105 | for (let j = 0; j < combos.length; j++) { 106 | remainingCombos.delete(combos[j]) 107 | } 108 | } 109 | return processedGroups 110 | } 111 | 112 | function reverseNonPairs(combos, suffix) { 113 | function toShortNotation(hash, group) { 114 | const first = group[0] 115 | if (group.length === 1) return first 116 | const last = group[group.length - 1] 117 | if (hash[first] === 0 && hash !== connectors && hash !== gappers) { 118 | return last + suffix + '+' 119 | } 120 | return first + suffix + '-' + last + suffix 121 | } 122 | 123 | const remainingCombos = new Set(combos) 124 | let results = [] 125 | for (let i = 0; i < orderedHashes.length; i++) { 126 | const hash = orderedHashes[i] 127 | const groups = processCombos(hash, remainingCombos) 128 | const processedGroups = processGroups(groups, remainingCombos) 129 | const notations = processedGroups.map(x => toShortNotation(hash, x)) 130 | results = results.concat(notations) 131 | } 132 | return results.concat(Array.from(remainingCombos).map(x => x + suffix)) 133 | } 134 | 135 | module.exports = reverseNonPairs 136 | -------------------------------------------------------------------------------- /prange.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { codeRanks, rankCodes, byCodeRankDescending } = require('./lib/core') 4 | 5 | const dashRangeRx = /[A,K,Q,J,T,2-9]{2}[o,s]?-[A,K,Q,J,T,2-9]{2}[o,s]?$/ 6 | const plusRangeRx = /[A,K,Q,J,T,2-9]{2}[o,s]?\+$/ 7 | const oneComboRx = /[A,K,Q,J,T,2-9]{2}[o,s]?$/ 8 | 9 | function minToMaxPairs(a, b) { 10 | const codea = rankCodes[a] 11 | const codeb = rankCodes[b] 12 | const min = Math.min(codea, codeb) 13 | const max = Math.max(codea, codeb) 14 | const arr = [] 15 | for (let i = min; i <= max; i++) { 16 | const rank = codeRanks[i] 17 | arr.push(rank + rank) 18 | } 19 | return arr 20 | } 21 | 22 | function minToMaxNonPairs([ a1, a2, suita ], [ b1, b2, suitb ]) { 23 | let bothSuits = suita == null && suitb == null 24 | if (!bothSuits) { 25 | if (suita == null) suita = suitb 26 | if (suitb == null) suitb = suita 27 | if (suita !== suitb) { 28 | throw new Error(`${a1}${a2}${suita}-${b1}${b2}${suitb} have different suits`) 29 | } 30 | } 31 | 32 | const codea = rankCodes[a2] 33 | const codeb = rankCodes[b2] 34 | const min = Math.min(codea, codeb) 35 | const max = Math.max(codea, codeb) 36 | const arr = [] 37 | for (let i = min; i <= max; i++) { 38 | const rank = codeRanks[i] 39 | if (bothSuits) { 40 | arr.push(a1 + rank + 'o') 41 | arr.push(a1 + rank + 's') 42 | } else { 43 | arr.push(a1 + rank + suita) 44 | } 45 | } 46 | return arr 47 | } 48 | 49 | function minToMaxConnectorsOrGappers([ a1, a2, suita ], [ b1, b2, suitb ]) { 50 | let bothSuits = suita == null && suitb == null 51 | if (!bothSuits) { 52 | if (suita == null) suita = suitb 53 | if (suitb == null) suitb = suita 54 | if (suita !== suitb) { 55 | throw new Error(`${a1}${a2}${suita}-${b1}${b2}${suitb} have different suits`) 56 | } 57 | } 58 | 59 | const codea1 = rankCodes[a1] 60 | const codea2 = rankCodes[a2] 61 | const codeb1 = rankCodes[b1] 62 | const codeb2 = rankCodes[b2] 63 | const min1 = Math.min(codea1, codeb1) 64 | const max1 = Math.max(codea1, codeb1) 65 | const min2 = Math.min(codea2, codeb2) 66 | const max2 = Math.max(codea2, codeb2) 67 | const delta1 = max1 - min1 68 | const delta2 = max2 - min2 69 | 70 | if (delta1 !== delta2) { 71 | throw new Error( 72 | `Connectors/Gappers ${a1}${a2}${suita}-${b1}${b2}${suitb} have unequal distance` 73 | ) 74 | } 75 | 76 | const arr = [] 77 | for (let i = min1, j = min2; i <= max1 && j <= max2; i++, j++) { 78 | const rank1 = codeRanks[i] 79 | const rank2 = codeRanks[j] 80 | if (bothSuits) { 81 | arr.push(rank1 + rank2 + 'o') 82 | arr.push(rank1 + rank2 + 's') 83 | } else { 84 | arr.push(rank1 + rank2 + suita) 85 | } 86 | } 87 | return arr 88 | } 89 | 90 | function singleCard([ a1, a2, suit ]) { 91 | if (suit == null) { 92 | return [ `${a1}${a2}o`, `${a1}${a2}s` ] 93 | } else { 94 | return [ `${a1}${a2}${suit}` ] 95 | } 96 | } 97 | 98 | function minToMax(a, b) { 99 | if (a === b) return singleCard(a) 100 | if (a[0] === a[1] && b[0] === b[1]) return minToMaxPairs(a[0], b[0]) 101 | if (a[0] === b[0] && a[1] !== b[1]) return minToMaxNonPairs(a, b) 102 | return minToMaxConnectorsOrGappers(a, b) 103 | } 104 | 105 | function plus([ a, b, suit ]) { 106 | if (a === b) return minToMaxPairs(a, 'A') 107 | 108 | // K9+ gets filled as K9-KQ, i.e. the `9` part is filled up to `Q` 109 | // There is no way to treat things like JT+ as meaning AK-JT since in that 110 | // case J9+ is ambiguous, i.e. is it AQ-J9 or JT-J9? 111 | // Therefore we always assume the + only applies to the second rank. 112 | const codea = rankCodes[a] 113 | const codeb = rankCodes[b] 114 | const max = Math.max(codea, codeb) 115 | const min = Math.min(codea, codeb) 116 | const suitString = suit == null ? '' : suit 117 | 118 | const maxHand = codeRanks[max] + codeRanks[max - 1] + suitString 119 | const minHand = codeRanks[max] + codeRanks[min] + suitString 120 | return minToMax(minHand, maxHand) 121 | } 122 | 123 | function subrange(s) { 124 | if (dashRangeRx.test(s)) { 125 | const parts = s.split(/-/).map(x => x.trim()) 126 | return minToMax(parts[0], parts[1]) 127 | } 128 | if (plusRangeRx.test(s)) { 129 | return plus(s.replace(/\+/, '')) 130 | } 131 | if (oneComboRx.test(s)) { 132 | const [ r1, r2, suit ] = s.trim() 133 | if (r1 === r2 || suit != null) return [ s ] 134 | return [ s + 'o', s + 's' ] 135 | } 136 | 137 | throw new Error(`Invalid range/combo specifier ${s}`) 138 | } 139 | 140 | /** 141 | * Converts a short notation for poker hand ranges into an array 142 | * filled with the matching combos. 143 | * 144 | * Each range specifier is separated by a comma. 145 | * 146 | * The following notations are supported: 147 | * 148 | * - single combos `KK, AK, ATs` 149 | * - plus notation 150 | * - `QQ+` = `[ AA, KK, QQ ]` 151 | * - `KTs+` = `[ KQs, KJs, KTs ]` 152 | * - `KTo+` = `[ KQo, KJo, KTo ]` 153 | * - `KT+` = `[ KQs, KQo, KJo, KJs, KTo, KTs ]` 154 | * - dash notation 155 | * - `KK-JJ` = `[ KK, QQ, JJ ]` 156 | * - `AKo-ATo` = `[ AK, AQ, AJ, AT ]` 157 | * - `AKs-JTs` = `[ AKs, KQs, JTs ]` 158 | * 159 | * @name prange 160 | * @function 161 | * @param {String} s the short notation for the range 162 | * @return {Array.} all hand combos satisfying the given range 163 | */ 164 | function prange(s) { 165 | const set = new Set() 166 | const subs = s 167 | // correct things like AJs -A9s to AJs-A9s 168 | .replace( 169 | /([A,K,Q,J,T,2-9]{2}[o,s]?)\s*-\s*([A,K,Q,J,T,2-9]{2}[o,s]?)/g 170 | , '$1-$2' 171 | ) 172 | // correct AK + to AK+ 173 | .replace( 174 | /([A,K,Q,J,T,2-9]{2}[o,s]?)\s\+/g 175 | , '$1+' 176 | ) 177 | // split at any white space or comma (any errornous space was removed via replace) 178 | .split(/[,\s]+/).map(x => x.trim()) 179 | for (let i = 0; i < subs.length; i++) { 180 | const res = subrange(subs[i]) 181 | res.forEach(x => set.add(x)) 182 | } 183 | return Array.from(set).sort(byCodeRankDescending) 184 | } 185 | 186 | module.exports = prange 187 | module.exports.reverse = require('./prange.reverse') 188 | module.exports.categorize = module.exports.reverse.sortOut 189 | --------------------------------------------------------------------------------