Home Reference Source

src/parse.js

import standard from '../static/standard-suffixes.json'
import longScale from '../static/long-scale-suffixes.json'
import {requireDecimal} from './decimal.js'

//const suffixGroups = {standard, longScale}
const suffixGroups = {standard} // TODO longscale parsing. There's a duplicate
const suffixGroupsToExp = {}
for (let groupName of Object.keys(suffixGroups)) {
  const group = suffixGroups[groupName]
  const suffixToExp = suffixGroupsToExp[groupName] = {}
  for (let fkey of Object.keys(group)) {
    const fg = group[fkey]
    for (let index in fg) {
      const suffix = fg[index].toLowerCase()
      const exp = index * 3
      if(suffixToExp[suffix] && suffixToExp[suffix].exp === exp,
        "duplicate parsenumber suffix with different exponents: "+suffix)
      suffixToExp[suffix] = {index, exp, replace:'e'+exp}
    }
  }
}

const backends = {
  'native': {
    parseInt(text, config) {
      const val = Math.ceil(Number(text, 10))
      return 'default' in config && !this.isValid(val) ? config['default'] : val
    },
    isValid(val) {
      return (val || val === 0) && !Number.isNaN(val)
    },
  },
  'decimal.js': {
    parseInt(text, config) {
      try {
        const Decimal = requireDecimal(config)
        const val0 = new Decimal(text)
        // decimal.js-light doesn't have ceil; use the more general rounding fn.
        // Not yet worth a separate adapter.
        const val = val0.ceil
          ? val0.ceil()
          : val0.toDecimalPlaces(0, Decimal.ROUND_UP)
        return this.isValid(val) ? val : config['default']
      }
      catch(e) {
        if ('default' in config) return config.default
        throw e
      }
    },
    isValid(val) {
      // decimal.js-light doesn't have isNaN(), it just throws. Test for isNaN only if it exists.
      return val && (!val.isNaN || !val.isNaN())
    },
  },
}

export function parse(text, config={}) {
  if (!text) return config['default'] || null
  // TODO make this an option
  const suffixToExp = suffixGroupsToExp[config.suffixGroup || 'standard']
  const backend = backends[config.backend || 'native']
  if (!backend) throw new Error('no such backend: '+config.backend)
  // remove commas. TODO: i18n fail
  text=text.replace(/,/g, '')
  // replace suffixes ('billion', etc)
  const match=/ ?[a-zA-Z]+/.exec(text)
  if (match && match.length > 0) {
    const exp = suffixToExp[match[0].toLowerCase()]
    if (exp) {
      // ceil(): buy at least this many. The default of floor() is annoying when
      // we're trying to purchase exactly-n for an upgrade.
      return backend.parseInt(text.replace(match[0], exp.replace), config)
    }
  }
  // no/invalid suffix found
  // note that we also get here for a suffix of 'e', like '1e3', because it's not
  // used as a suffix. Decimal.js will parse it.
  return backend.parseInt(text, config)
}

export class Parser {
  constructor(config) {
    this.config = config
  }
  parse(text, config={}) {
    return parse(text, {...this.config, ...config})
  }
}