From 4b4a59f2304c474bbcfa83d160cd11470d036694 Mon Sep 17 00:00:00 2001 From: Mia Nordentoft Date: Thu, 6 Jun 2019 12:18:15 +0200 Subject: [PATCH] Move lib to vocho-lib, show disqualified candidates in ranked pairs, bump version --- build.sh | 27 --- package-lock.json | 8 +- package.json | 9 +- src/calc.js | 11 +- src/index.js | 3 +- src/lib/index.js | 4 - src/lib/ranked-pairs.js | 392 ---------------------------------------- src/lib/stv.js | 293 ------------------------------ src/lib/util.js | 18 -- 9 files changed, 20 insertions(+), 745 deletions(-) delete mode 100644 src/lib/index.js delete mode 100644 src/lib/ranked-pairs.js delete mode 100644 src/lib/stv.js delete mode 100644 src/lib/util.js diff --git a/build.sh b/build.sh index e981926..11a543b 100755 --- a/build.sh +++ b/build.sh @@ -11,12 +11,6 @@ if [ ! -d "node/node-v$VERSION-linux-x64" ]; then wget -qO- "https://nodejs.org/dist/v$VERSION/node-v$VERSION-linux-x64.tar.xz" | tar -xJf - -C node fi -# Darwin x64 -if [ ! -d "node/node-v$VERSION-darwin-x64" ]; then - echo Downloading node darwin-x64 ... - wget -qO- "https://nodejs.org/dist/v$VERSION/node-v$VERSION-darwin-x64.tar.gz" | tar -xzf - -C node -fi - # Build zip files @@ -40,24 +34,3 @@ echo Tarballing linux-x64 ... tar -cJf ../linux-x64.tar.xz * echo Cleaning up from linux-x64 cd .. && rm -rf linux-x64 - -# Darwin x64 -echo Creating darwin-x64 files ... -mkdir -p darwin-x64 && cd $_ -cp -r "../node/node-v$VERSION-darwin-x64" node -rsync -a ../../ vocho --exclude dist --exclude .git --exclude node_modules -cat >run.sh < { }); screen.key('C-x', () => process.exit(0)); screen.key('f12', () => { + screen.program.hideCursor(); helpScreen.toggle(); screen.render(); }); diff --git a/src/lib/index.js b/src/lib/index.js deleted file mode 100644 index e14274e..0000000 --- a/src/lib/index.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - RankedPairs: require('./ranked-pairs'), - STV: require('./stv') -}; diff --git a/src/lib/ranked-pairs.js b/src/lib/ranked-pairs.js deleted file mode 100644 index 8d28f2f..0000000 --- a/src/lib/ranked-pairs.js +++ /dev/null @@ -1,392 +0,0 @@ -const util = require('./util'); - -// https://hackernoon.com/the-javascript-developers-guide-to-graphs-and-detecting-cycles-in-them-96f4f619d563 -function isCyclic (graph) { - const nodes = Object.keys(graph); - const visited = {}; - const recStack = {}; - - const _isCyclic = (node, visited, recStack) => { - if (!visited[node]) { - visited[node] = true; - recStack[node] = true; - const nodeNeighbors = graph[node]; - for (let currentNode of nodeNeighbors) { - if ( - (!visited[currentNode] && _isCyclic(currentNode, visited, recStack)) || - recStack[currentNode] - ) { return true; } - } - } - recStack[node] = false; - return false; - }; - - for (let node of nodes) { - if (_isCyclic(node, visited, recStack)) { - return true; - } - } - return false; -} - -/** - * Runs a tideman ranked pairs election - * @param {string[]|string} candidates The candidates. Each candidate must be represented by one character - * @param {string[]} ballots All ballots written using tideman ranked pairs syntax - * @param {string[]} [ignoredCandidates] An array of candidates to ignore - * @param {string} [tieBreaker] The fully inclusive tie breaker ballot without any equals - * @return {Object} - */ -function RankedPairs (candidates, ballots, ignoredCandidates = [], tieBreaker) { - if (typeof candidates === 'string') { candidates = candidates.split(''); } - candidates.sort(); - - let tieBreakerArr = []; - if (tieBreaker) { - tieBreakerArr = tieBreaker.split('>'); - - if (new Set(tieBreakerArr).size !== tieBreakerArr.length) { - const err = new Error('The tie breaker ballot must not contain duplicate candidates'); - err.type = 'INVALID_TIE_BREAKER'; - throw err; - } - - if (tieBreakerArr.length < candidates.length) { - const err = new Error('The tie breaker ballot must contain all candidates'); - err.type = 'INVALID_TIE_BREAKER'; - throw err; - } - - for (let cand of tieBreakerArr) { - if (!candidates.includes(cand)) { - const err = new Error(`Invalid candidate ${cand} in tie breaker`); - err.type = 'INVALID_TIE_BREAKER'; - throw err; - } - } - } - - // Create pairs - const pairs = {}; - for (let i = 0; i < candidates.length; i++) { - const cand1 = candidates[i]; - for (let n = i + 1; n < candidates.length; n++) { - const cand2 = candidates[n]; - const pairName = cand1 + cand2; - const pair = pairs[pairName] = { - diff: 0, - winner: null, - loser: null - }; - pair[cand1] = 0; - pair[cand2] = 0; - } - } - - // Tally - let blankBallots = 0; - const candStats = {}; - for (let cand of candidates) { - candStats[cand] = { - won: 0, - lost: 0, - mentions: 0 - }; - } - - for (let ballot of ballots) { - const alreadyMentioned = []; - - const rows = ballot - .split('>') - .filter(row => row.length) // Turn blank votes into an empty array - .map(row => row.split('=')); - - if (!rows.length) { - blankBallots++; - continue; - } - - for (let y = 0; y < rows.length; y++) { - const curRow = rows[y]; - - for (let curCol of curRow) { - if (!candidates.includes(curCol)) { - const err = new Error(`Invalid candidate ${curCol} in ballot ${ballot}`); - err.type = 'INVALID_BALLOT'; - throw err; - } - if (alreadyMentioned.includes(curCol)) { - const err = new Error(`Duplicate candidate ${curCol} in ballot ${ballot}`); - err.type = 'INVALID_BALLOT'; - throw err; - } - alreadyMentioned.push(curCol); - candStats[curCol].mentions++; - - for (let i = y + 1; i < rows.length; i++) { - const lesserRow = rows[i]; - - for (let lesserCol of lesserRow) { - if (lesserCol === curCol) { - const err = new Error(`Duplicate candidate ${curCol} in ballot ${ballot}`); - err.type = 'INVALID_BALLOT'; - throw err; - } - - const pairName = [ curCol, lesserCol ].sort().join(''); - pairs[pairName][curCol]++; - } - } - } - } - } - - // Check blank vote count - util.debug(`${ballots.length} ballots cast (${blankBallots} blank)`); - if (blankBallots >= ballots.length / 2) { - const err = new Error('Too many blank ballots'); - err.type = 'BLANK_BALLOTS'; - err.blankBallots = blankBallots; - err.numBallots = ballots.length; - throw err; - } - - // Disqualify candidates as needed - for (let [cand, stats] of Object.entries(candStats)) { - const isIgnored = ignoredCandidates.includes(cand); - const hasInsufficientMentions = stats.mentions < ballots.length / 2; - - if (isIgnored || hasInsufficientMentions) { - candidates.splice(candidates.indexOf(cand), 1); - - for (let pairName in pairs) { - const cands = pairName.split(''); - if (cands.includes(cand)) { - delete pairs[pairName]; - } - } - } - - if (isIgnored) { - util.debug(`${cand} is ignored in this election`); - } else if (hasInsufficientMentions) { - util.debug(`${cand} is disqualified due to insufficient mentions`); - } - } - - // Determine the results of the compared pairs - for (let [pairName, pair] of Object.entries(pairs)) { - const [cand1, cand2] = pairName.split(''); - pair.diff = pair[cand1] - pair[cand2]; - - if (pair[cand1] > pair[cand2]) { - candStats[cand1].won++; - candStats[cand2].lost++; - pair.winner = cand1; - pair.loser = cand2; - } else if (pair[cand2] > pair[cand1]) { - candStats[cand2].won++; - candStats[cand1].lost++; - pair.winner = cand2; - pair.loser = cand1; - } else { - if (!tieBreaker) { - const err = new Error('Tie breaker needed!'); - err.type = 'TIE_BREAKER_NEEDED'; - throw err; - } - - const cand1Index = tieBreakerArr.indexOf(cand1); - const cand2Index = tieBreakerArr.indexOf(cand2); - - if (cand1Index < cand2Index) { - candStats[cand1].won++; - candStats[cand2].lost++; - pair.winner = cand1; - pair.loser = cand2; - } else { - candStats[cand2].won++; - candStats[cand1].lost++; - pair.winner = cand2; - pair.loser = cand1; - } - } - } - - util.debug('\nCompared pairs:'); - util.debug(pairs); - - util.debug('\nCandidate pair scores:'); - util.debug(candStats); - - // Order the pairs - const orderedEntries = []; - const entries = Object.entries(pairs); - while (entries.length) { - let maxDiff = -1; - let maxDiffIndices = null; - for (let i = 0; i < entries.length; i++) { - const pair = entries[i]; - const absDiff = Math.abs(pair[1].diff); - if (absDiff > maxDiff) { - maxDiff = absDiff; - maxDiffIndices = [ i ]; - } else if (absDiff === maxDiff) { - maxDiffIndices.push(i); - } - } - - if (maxDiffIndices.length === 1) { - // No tie - const pair = entries.splice(maxDiffIndices[0], 1)[0]; - orderedEntries.push(pair); - } else { - // We have a tie, follow §2.10 - // Obtain the pairs, from the highest index to the lowest as to not mess up the indices when removing them - maxDiffIndices.sort((a, b) => b - a); - const equalPairs = maxDiffIndices.map(i => entries.splice(i, 1)[0]); - - // 1. The pair with a loser that's already listed as a loser is put first - const loserEntries = []; // All losers that are already in the ordered pairs - for (let i = 0; i < equalPairs.length; i++) { - const equalPair = equalPairs[i]; - // Find the loser of the equal pair - const equalPairLoser = equalPair[1].loser; - - // Check if the loser is already in the ordered pairs as a loser - let hasOrderedLoser = false; - let orderedIndex; - for (orderedIndex = 0; orderedIndex < orderedEntries.length; orderedIndex++) { - const orderedEntry = orderedEntries[orderedIndex]; - const orderedLoser = orderedEntry[1].loser; - if (equalPairLoser === orderedLoser) { - hasOrderedLoser = true; - break; - } - } - if (hasOrderedLoser) { loserEntries.push({ eqI: i, or: orderedIndex }); } - } - loserEntries.sort((a, b) => b.or - a.or); // Don't mess up the indices when splicing - - const newOrderedLoserEntries = []; - for (let i = 0; i < loserEntries.length; i++) { - const loserEntry = loserEntries[i]; - const nextLoserEntry = loserEntries[i + 1]; - if (typeof nextLoserEntry === 'undefined' || nextLoserEntry.or > loserEntry.or) { - newOrderedLoserEntries.push(loserEntry.eqI); - } - } - newOrderedLoserEntries.sort((a, b) => b - a); // Don't mess up the indices when splicing - for (let i of newOrderedLoserEntries) { - orderedEntries.push(equalPairs.splice(i, 1)[0]); - } - - // 2. The pair with a winner that's already listed as a winner is put first - const winnerEntries = []; // All winners that are already in the ordered pairs - for (let i = 0; i < equalPairs.length; i++) { - const equalPair = equalPairs[i]; - // Find the winner of the equal pair - const equalPairWinner = equalPair[1].winner; - - // Check if the winner is already in the ordered pairs as a winner - let hasOrderedWinner = false; - let orderedIndex; - for (orderedIndex = 0; orderedIndex < orderedEntries.length; orderedIndex++) { - const orderedEntry = orderedEntries[orderedIndex]; - const orderedWinner = orderedEntry[1].winner; - if (equalPairWinner === orderedWinner) { - hasOrderedWinner = true; - break; - } - } - if (hasOrderedWinner) { winnerEntries.push({ eqI: i, or: orderedIndex }); } - } - winnerEntries.sort((a, b) => b.or - a.or); // Don't mess up the indices when splicing - - const newOrderedWinnerEntries = []; - for (let i = 0; i < winnerEntries.length; i++) { - const winnerEntry = winnerEntries[i]; - const nextWinnerEntry = winnerEntries[i + 1]; - if (typeof nextWinnerEntry === 'undefined' || nextWinnerEntry.or > winnerEntry.or) { - newOrderedWinnerEntries.push(winnerEntry.eqI); - } - } - newOrderedWinnerEntries.sort((a, b) => b - a); // Don't mess up the indices when splicing - for (let i of newOrderedWinnerEntries) { - orderedEntries.push(equalPairs.splice(i, 1)[0]); - } - - if (equalPairs.length > 1) { - // 3. The pair with a loser that is least preferred by the tie breaker ballot is put first - if (!tieBreaker) { - const err = new Error('Tie breaker needed!'); - err.type = 'TIE_BREAKER_NEEDED'; - throw err; - } - - const loserPrefIndices = equalPairs.map((equalPairEntry, i) => { - const loser = equalPairEntry[1].loser; - return { eqI: i, or: tieBreakerArr.indexOf(loser) }; - }); - - const newOrderedTieBreakerPairs = []; - for (let i = 0; i < loserPrefIndices.length; i++) { - const pair = loserPrefIndices[i]; - const nextPair = loserPrefIndices[i + 1]; - if (typeof nextPair === 'undefined' || nextPair.or > pair.or) { - newOrderedTieBreakerPairs.push(pair.eqI); - } - newOrderedTieBreakerPairs.sort((a, b) => b - a); // Don't mess up the indices when splicing - for (let i of newOrderedTieBreakerPairs) { - orderedEntries.push(equalPairs.splice(i, 1)[0]); - } - } - } - - // There should only be one pair remaining at this point - orderedEntries.push(...equalPairs); - } - } - util.debug('\nCompared pairs:'); - util.debug(orderedEntries); - - // Make a graph of the winning pairs - let lock = {}; - for (let cand of candidates) { - lock[cand] = []; - } - - util.debug('\nLock'); - for (let entry of orderedEntries) { - const pair = entry[1]; - const from = pair.winner; - const to = pair.loser; - - const newLock = {...lock}; - newLock[from].push(to); - if (isCyclic(newLock)) { continue; } - lock = newLock; - - util.debug(`${from} → ${to}`); - } - - // Find the candidate at the root of the graph (with nothing pointing to it) - const possibleWinners = [...candidates]; - const candsPointedTo = new Set([].concat(...Object.values(lock))); - for (let cand of candsPointedTo) { - possibleWinners.splice(possibleWinners.indexOf(cand), 1); - } - const winner = possibleWinners[0]; - - util.debug(`\nWinner: ${winner}`); - - return { - ballots: ballots.length, - blankBallots: blankBallots, - winner: winner - }; -} - -module.exports = RankedPairs; diff --git a/src/lib/stv.js b/src/lib/stv.js deleted file mode 100644 index 521a3d1..0000000 --- a/src/lib/stv.js +++ /dev/null @@ -1,293 +0,0 @@ -const util = require('./util'); - -/** - * Runs a Single Transferable Vote election - * @param {number} places The number of electable candidates (seats) - * @param {string[]|string} candidates The candidates. Each candidate must be represented by one character - * @param {string[]} ballots All ballots - * @param {string[]} [ignoredCandidates] [description] - * @param {string} [tieBreaker] A tie breaker listing all candidates - * @returns {Object} - */ -function STV (places, candidates, ballots, ignoredCandidates = [], tieBreaker) { - if (typeof candidates === 'string') { candidates = candidates.split(''); } - places = Math.min(places, candidates.length); // We can't elect a ghost - - // Validate the tie breaker - if (typeof tieBreaker !== 'undefined') { - for (let pref of tieBreaker) { - if (new Set(tieBreaker).size !== tieBreaker.length) { - const err = new Error('Duplicate candidates in tie breaker'); - err.type = 'INVALID_TIE_BREAKER'; - throw err; - } - if (tieBreaker.length !== candidates.length) { - const err = new Error('Tie breaker vote must contain all candidates'); - err.type = 'INVALID_TIE_BREAKER'; - throw err; - } - if (!candidates.includes(pref)) { - const err = new Error(`Invalid candidate ${pref} in tie breaker`); - err.type = 'INVALID_TIE_BREAKER'; - throw err; - } - } - for (let cand of ignoredCandidates) { - tieBreaker = tieBreaker.replace(cand, ''); - } - } - - const originalBallots = [ ...ballots ]; - const weightedBallots = ballots.map(ballot => { - return { - weight: 1, - prefs: ballot - }; - }); - - const quota = ballots.length / (places + 1); // Hagenbach-Bischoff - - let blankBallots = 0; - - // Validate the ballots - for (let i in ballots) { - const ballot = ballots[i]; - - if (ballot.length === 0) { - blankBallots++; - continue; - } - - const alreadyMentioned = []; - for (let pref of ballot) { - if (!candidates.includes(pref)) { - const err = new Error(`Invalid candidate ${pref} in ballot ${ballot}`); - err.type = 'INVALID_BALLOT'; - throw err; - } - if (alreadyMentioned.includes(pref)) { - const err = new Error(`Duplicate candidates ${pref} in ballot ${ballot}`); - err.type = 'INVALID_BALLOT'; - throw err; - } - alreadyMentioned.push(pref); - } - - for (let cand of ignoredCandidates) { - ballots[i] = ballot.replace(cand, ''); - } - } - - for (let cand of ignoredCandidates) { - candidates.splice(candidates.indexOf(cand), 1); - } - - // Check blank vote count - util.debug(`${ballots.length} ballots cast (${blankBallots} blank)`); - if (blankBallots >= ballots.length / 2) { - const err = new Error('Too many blank ballots'); - err.type = 'BLANK_BALLOTS'; - err.numBallots = ballots.length; - err.blankBallots = blankBallots; - throw err; - } - - util.debug(`There are ${places} places and ${candidates.length} candidates`); - util.debug(`Election quota: ${quota}`); - - const electedCandidates = []; - // Determine the amount of votes each candidate has based on everyone's first preference - const candidateVotes = {}; - for (let cand of candidates) { candidateVotes[cand] = 0; } - for (let ballot of ballots) { - const firstPref = ballot[0]; - candidateVotes[firstPref]++; - } - - let round = 0; - while (electedCandidates.length < places && round < 6) { // TODO: Remove round limit - util.debug(`\nRound ${round + 1}:`); - round++; - - util.debug(`Valid candidates: ${candidates.join(', ')}`); - - const votesDebug = []; - const exceedsQuota = []; - util.debug('Votes for each candidate:'); - for (let [cand, votes] of Object.entries(candidateVotes)) { - votesDebug.push(`${cand}: ${votes}`); - if (votes > quota) { exceedsQuota.push(cand); } - } - util.debug(votesDebug.join(', ')); - electedCandidates.push(...exceedsQuota); - - util.debug('Ballots:'); - util.debug(weightedBallots); - - // § 3.7: Check if the amount of remaining candidates is equal to the amount of remaining places, and if so elect all remaining candidates - if (places - electedCandidates.length === candidates.length) { - // Elect all remaining candidates - electedCandidates.push(...candidates); - util.debug(`Elected all remaining candidates: ${candidates.join(', ')}`); - break; - } - - if (exceedsQuota.length) { - util.debug(`Elected candidates: ${exceedsQuota.join(', ')}`); - } else { - util.debug('No candidates elected'); - } - - // Transfer surplus votes - // Calculate the surplus transfer value using the Gregory method - for (let cand of exceedsQuota) { - const votesReceived = candidateVotes[cand]; - - // Find all ballots that listed the candidate as the first priority - const firstPrefBallots = []; - for (let ballot of weightedBallots) { - if (ballot.prefs[0] !== cand) { continue; } - firstPrefBallots.push(ballot); - } - const totalCandVoteValue = firstPrefBallots.map(b => b.weight).reduce((a, b) => a + b, 0); - const transferValueFactor = (totalCandVoteValue - quota) / totalCandVoteValue; - - for (let ballot of firstPrefBallots) { - // Change the value of each relevant ballot - ballot.weight *= transferValueFactor; - } - - // Remove elected candidates from the array of candidates - candidates.splice(candidates.indexOf(cand), 1); - delete candidateVotes[cand]; - - // Remove all mentions of the candidate from ballots - for (let ballot of weightedBallots) { - ballot.prefs = ballot.prefs.replace(cand, ''); - } - - const transferTo = {}; - for (let ballot of firstPrefBallots) { - // Count the second priorities of all relevant ballots - const nextPref = ballot.prefs[0]; - if (!nextPref) { continue; } // Ignore the vote if there's no next priority - if (!(nextPref in transferTo)) { transferTo[nextPref] = 1; } - else { transferTo[nextPref]++; } - } - - // Transfer the votes - for (let [to, votes] of Object.entries(transferTo)) { - const newVotes = (votesReceived - quota) / votesReceived * votes; - candidateVotes[to] += newVotes; - } - } - - if (!exceedsQuota.length) { // No candidate elected, time to eliminate someone - // § 3.11, eliminate the candidate with the least voices - let minVotes = Number.MAX_SAFE_INTEGER; - let minVotesCands = null; - for (let [cand, votes] of Object.entries(candidateVotes)) { - if (votes < minVotes) { - minVotes = votes; - minVotesCands = [ cand ]; - } else if (votes === minVotes) { - minVotesCands.push(cand); - } - } - - let eliminatedCand; - if (minVotesCands.length === 1) { // No tie - eliminatedCand = minVotesCands[0]; - } else { - // § 3.11 If multiple candidates have the same amount of votes, eliminate the one with the least first priorities, - // then second priorities etc. in the ORIGINAL ballots. - // If there is still equality, a tie breaker is needed, whose least preferred of the relevant candidates is to be eliminated - - let priorityNum = -1; - while (minVotesCands.length > 1 && priorityNum < candidates.length) { - priorityNum++; - - let numPriorities = Number.MAX_SAFE_INTEGER; - let numPrioritiesCands = null; - for (let cand of minVotesCands) { - // Find all ballots with the candidate at this priority level - let candNumPriorities = 0; - for (let ballot of originalBallots) { - if (ballot[priorityNum] !== cand) { continue; } - candNumPriorities++; - } - if (candNumPriorities < numPriorities) { - numPriorities = candNumPriorities; - numPrioritiesCands = [ cand ]; - } else { - numPrioritiesCands.push(cand); - } - } - - if (numPrioritiesCands.length === 1) { - minVotesCands = numPrioritiesCands; - } - } - - // Check if we've found a candidate to eliminate - if (minVotesCands.length === 1) { - eliminatedCand = minVotesCands[0]; - } else { - // Nope, there's still equality. This calls for a tie breaker - if (!tieBreaker) { - const err = new Error('Tie breaker needed!'); - err.type = 'TIE_BREAKER_NEEDED'; - throw err; - } - // The least preferred candidate according to the tie breaker is eliminated - const preferenceIndices = minVotesCands.map(cand => { - return { - cand: cand, - index: tieBreaker.indexOf(cand) - }; - }); - eliminatedCand = preferenceIndices - .reduce((a, b) => { - if (a.index > b.index) { return a; } - return b; - }) - .cand; - } - } - - // Transfer the voices of the eliminated candidate - for (let ballot of weightedBallots) { - // Find all ballots that have the eliminated candidate as their first priority - if (ballot.prefs[0] !== eliminatedCand) { continue; } - // Find their next preference - const nextPref = ballot.prefs[1]; - if (!nextPref) { continue; } // Unless there is none - candidateVotes[nextPref] += ballot.weight; - } - - // Remove eliminated candidates from the array of candidates - candidates.splice(candidates.indexOf(eliminatedCand), 1); - delete candidateVotes[eliminatedCand]; - - // Remove all mentions of the candidate from ballots - for (let ballot of weightedBallots) { - ballot.prefs = ballot.prefs.replace(eliminatedCand, ''); - } - - util.debug(`Eliminated candidate: ${eliminatedCand}`); - } - } - - util.debug(`\n\nDone!\nElected: ${electedCandidates.join(', ')}`); - - util.debug('Remaining ballots:'); - util.debug(weightedBallots); - - return { - ballots: ballots.length, - blankBallots: blankBallots, - winners: electedCandidates - }; -} - -module.exports = STV; diff --git a/src/lib/util.js b/src/lib/util.js deleted file mode 100644 index 4060612..0000000 --- a/src/lib/util.js +++ /dev/null @@ -1,18 +0,0 @@ -const ENV = process.env.NODE_ENV || 'dev'; - -function debug (...obj) { - if (ENV === 'dev' && !global.disableDebug) { - return console.log(...obj); - } -} - -function debugOneLine (...obj) { - if (ENV === 'dev' && !global.disableDebug) { - return process.stdout.write(...obj); - } -} - -module.exports = { - debug: debug, - debugOneLine: debugOneLine -};