Skip to content

Commit

Permalink
Merge pull request #1 from faradayio/assume-number-1
Browse files Browse the repository at this point in the history
Handle more cases
  • Loading branch information
f3rno64 authored Jun 11, 2024
2 parents 6a1161c + 106de8d commit 56554b3
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 142 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
],
"scripts": {
"docs": "npx typedoc --out docs src && cp LICENSE.md docs/LICENSE.md && cp CHANGELOG.md docs/CHANGELOG.md",
"test": "NODE_PATH=./src NODE_ENV=test mocha",
"test": "NODE_PATH=./src NODE_ENV=test mocha --bail",
"test:coverage": "NODE_PATH=./src NODE_ENV=test nyc mocha",
"test:watch": "NODE_PATH=./src NODE_ENV=test nyc mocha --watch",
"test:watch": "NODE_PATH=./src NODE_ENV=test nyc mocha --watch --bail",
"build": "NODE_PATH=./src tsc -p tsconfig.json",
"lint": "eslint -f unix \"src/**/*.{ts,tsx}\"",
"update-deps": "updates -u -g -c",
Expand Down
17 changes: 0 additions & 17 deletions src/const.ts

This file was deleted.

163 changes: 114 additions & 49 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,41 @@
import * as U from './utils'
import { InvalidInputError } from './errors'
import { Numbers, Magnitudes, Multiples } from './types'
import { NUMBER_WORDS, MAGNITUDE_WORDS, MULTIPLES_WORDS } from './const'
const NUMBERS = {
'zero': 0,
'one': 1,
'two': 2,
'three': 3,
'four': 4,
'five': 5,
'six': 6,
'seven': 7,
'eight': 8,
'nine': 9,
'ten': 10,
'eleven': 11,
'twelve': 12,
'thirteen': 13,
'fourteen': 14,
'fifteen': 15,
'sixteen': 16,
'seventeen': 17,
'eighteen': 18,
'nineteen': 19,
'twenty': 20,
'thirty': 30,
'forty': 40,
'fourty': 40,
'fifty': 50,
'sixty': 60,
'seventy': 70,
'eighty': 80,
'ninety': 90,
'hundred': 100,
'thousand': 1000,
'million': 1000000,
'billion': 1000000000,
'trillion': 1000000000000,
'quadrillion': 1000000000000000
}


/**
* Parses a string defining a number with words into its numeric value.
Expand Down Expand Up @@ -30,64 +64,95 @@ import { NUMBER_WORDS, MAGNITUDE_WORDS, MULTIPLES_WORDS } from './const'
* @public
*/
const parse = (input: string): number => {
const words = input.replace(/-/g, ' ').split(' ')

let result = 0
let currentMultiple: number | null = null
let currentQuantity: number | null = null

for (let i = 0; i < words.length; i++) {
const word = words[i].toLowerCase().replace(/\W/g, '')
const words = input.toLowerCase().replace(/[-\s]+/g, ' ').replace(/[^\w ]+/g, '').split(' ')
const numbers: (number | true)[] = []

// first convert all the words to numbers
for (const word of words) {
if (word === 'and') {
if (currentQuantity !== null) {
result += currentQuantity
currentQuantity = null
}
numbers.push(true)
} else if (word as keyof typeof NUMBERS in NUMBERS) {
numbers.push(NUMBERS[word as keyof typeof NUMBERS])
}
}

// then group the numbers by the rule that a smaller number means a new set
// each set will be multiplied together
const sets: Array<number[]> = []
let currentSet: number[] = []
let lastNumber: number | null = null
for (let i = 0; i < numbers.length; i++) {
const number = numbers[i]
if (number === true) {
// this is a weird case where we have a number like "one hundred and twenty three"
sets.push(currentSet)
currentSet = []
continue
}
if (lastNumber !== null && lastNumber > number) {
// next number is smaller, so you've arrived at the end of a set
sets.push(currentSet)
currentSet = []
}
if (typeof number === 'number') {
currentSet.push(number)
lastNumber = number
}
}

const capitalizedWord = U.capitalize(word)

if (MULTIPLES_WORDS.includes(capitalizedWord)) {
if (currentMultiple !== null) {
throw new InvalidInputError(input, 'parsed two multiples in a row')
} else if (currentQuantity !== null) {
throw new InvalidInputError(input, 'parsed a number before a multiple')
}

currentMultiple = Multiples[capitalizedWord as keyof typeof Multiples]
} else if (NUMBER_WORDS.includes(capitalizedWord)) {
const value = Numbers[capitalizedWord as keyof typeof Numbers]

if (currentQuantity !== null) {
throw new InvalidInputError(input, 'parsed two numbers in a row')
}
// make sure you clear the buffer
if (currentSet.length > 0) {
sets.push(currentSet)
}

if (currentMultiple !== null) {
result += currentMultiple + value
currentMultiple = null
} else {
currentQuantity = value
// nine one -> 91
for (let i = 0; i < sets.length - 1; i++) {
const set = sets[i]
const nextSet = sets[i + 1]
if (set.length === 1 && nextSet.length === 1 && set[0] < 10 && nextSet[0] < 100) {
// console.log('combining', set, nextSet)
let str = ''
for (const n of set) {
str += n
}
} else if (MAGNITUDE_WORDS.includes(capitalizedWord)) {
const value = Magnitudes[capitalizedWord as keyof typeof Magnitudes]

if (currentQuantity === null) {
throw new InvalidInputError(input, 'parsed magnitude without number')
for (const n of nextSet) {
str += n
}
sets[i] = [parseInt(str)]
sets[i + 1] = []
}
}

result += currentQuantity * value
currentQuantity = null
currentMultiple = null
// when the last entry of a set is zero, remove the zero
// twenty zero -> 20
for (const set of sets) {
if (set.length > 1 && set[set.length - 1] === 0) {
// console.log('removing zero', set)
set.pop()
}
}

if (currentQuantity !== null) {
result += currentQuantity
} else if (currentMultiple !== null) {
result += currentMultiple
// finally combine the result, following weird english rules when necessary
let result = 0
for (const set of sets) {
if (set.length === 0) {
// empty set means it was an and
// don't reduce it with 1
continue
}
if (set.length > 1 && set.every((n) => n < 100)) {
// if all numbers are less than 19, combine them like a string
// one two -> 12
// one twenty -> 120
let str = ''
for (const n of set) {
str += n
}
result += parseInt(str)
continue
}
const product = set.reduce((a, b) => a * b, 1)
result += product
}

return result
Expand Down
150 changes: 139 additions & 11 deletions src/tests/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,68 @@
import { expect } from 'chai'

import parse from '../parse'
import { InvalidInputError } from '../errors'
import { Numbers, Magnitudes, Multiples } from '../types'
import { NUMBER_WORDS, MAGNITUDE_WORDS, MULTIPLES_WORDS } from '../const'

enum Numbers {
Zero = 0,
One = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6,
Seven = 7,
Eight = 8,
Nine = 9
}

// enum Teens {
// Ten = 10,
// Eleven = 11,
// Twelve = 12,
// Thirteen = 13,
// Fourteen = 14,
// Fifteen = 15,
// Sixteen = 16,
// Seventeen = 17,
// Eighteen = 18,
// Nineteen = 19
// }

enum Multiples {
Twenty = 20,
Thirty = 30,
Forty = 40,
Fifty = 50,
Sixty = 60,
Seventy = 70,
Eighty = 80,
Ninety = 90
}

enum Magnitudes {
Hundred = 100,
Thousand = 1000,
Million = 1000000,
Billion = 1000000000,
Trillion = 1000000000000,
Quadrillion = 1000000000000000
}

const NUMBER_WORDS = Object.keys(Numbers).filter(
(key: string): boolean => !Number.isFinite(+key)
)

const MAGNITUDE_WORDS = Object.keys(Magnitudes).filter(
(key: string): boolean => !Number.isFinite(+key)
)

const MULTIPLES_WORDS = Object.keys(Multiples).filter(
(key: string): boolean => !Number.isFinite(+key)
)

// const TEENS_WORDS = Object.keys(Teens).filter(
// (key: string): boolean => !Number.isFinite(+key)
// )

describe('parse', () => {
for (let i = 0; i < NUMBER_WORDS.length; i++) {
Expand Down Expand Up @@ -93,23 +152,92 @@ describe('parse', () => {
}
}

it('throws an error if the input contains two multiples in a row', () => {
expect(() => parse('twenty thirty')).to.throw(InvalidInputError)

it('parses one two', () => {
expect(parse('one two')).to.equal(12)
})

it('parses million', () => {
expect(parse('million')).to.equal(1000000)
})

it('parses nine one', () => {
expect(parse('nine one')).to.equal(91)
})

it('parses nine twenty one', () => {
expect(parse('nine twenty one')).to.equal(921)
})

it('parses four hundred five', () => {
expect(parse('four hundred five')).to.equal(405)
})

it('throws an error if the input contains two numbers in a row', () => {
expect(() => parse('one two')).to.throw(InvalidInputError)
it('parses a number before a multiple like one twenty', () => {
expect(parse('one twenty')).to.equal(120)
})

it('throws an error if the input has a magnitude but no quantity', () => {
expect(() => parse('million')).to.throw(InvalidInputError)
it('parses a number before a multiple like two ninety nine', () => {
expect(parse('two ninety nine')).to.equal(299)
})

it('throws an error if the input has a number before a multiple', () => {
expect(() => parse('one twenty')).to.throw(InvalidInputError)
it('parses a number like two thousand', () => {
expect(parse('two thousand')).to.equal(2000)
})

it('parses a number like two thousand ninety nine', () => {
expect(parse('two thousand ninety nine')).to.equal(2099)
})

it('parses a number like thirty thirty', () => {
expect(parse('thirty thirty')).to.equal(3030)
})

it('parses a single multiple', () => {
expect(parse('thirty')).to.equal(30)
})

// below tests generated by chatgpt

it('parses very large numbers like one million two hundred thousand', () => {
expect(parse('one million two hundred thousand')).to.equal(1200000)
})

it('parses complex numbers like twelve hundred thirty four', () => {
expect(parse('twelve hundred thirty four')).to.equal(1234)
})

it('parses numbers in the hundreds followed by a large scale like eight hundred thousand', () => {
expect(parse('eight hundred thousand')).to.equal(800000)
})

it('parses large numbers without "and" like six thousand five hundred', () => {
expect(parse('six thousand five hundred')).to.equal(6500)
})

it('parses numbers with "teen" and tens like seventeen fifty', () => {
expect(parse('seventeen fifty')).to.equal(1750)
})

it('parses numbers with "and" like two thousand and sixty', () => {
expect(parse('two thousand and sixty')).to.equal(2060)
})

it('parses numbers combining thousands and hundreds like three thousand four hundred', () => {
expect(parse('three thousand four hundred')).to.equal(3400)
})

it('parses large mixed scale numbers like fifteen hundred sixty', () => {
expect(parse('fifteen hundred sixty')).to.equal(1560)
})

it('parses sequential numbers like sixty seventy', () => {
expect(parse('sixty seventy')).to.equal(6070)
})

it('parses a number like four hundred fifty', () => {
expect(parse('four hundred fifty')).to.equal(450)
})


})
Loading

0 comments on commit 56554b3

Please sign in to comment.