Skip to content

Commit

Permalink
feat(parsers/integer): add integer parser
Browse files Browse the repository at this point in the history
See tests for examples.
  • Loading branch information
norskeld committed Nov 28, 2021
1 parent a007d13 commit f206d49
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 0 deletions.
52 changes: 52 additions & 0 deletions src/internal/parsers/integer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { success, Parser, State } from '../state'

import { regexp } from '../combinators/regexp'

type SignKind = 'any' | 'some' | 'positive' | 'negative' | 'none'
type SignMeta = [rexpression: RegExp, expectation: string]

interface IntegerOptions {
sign?: SignKind
radix?: number
}

const INT_SOME_RE = /^[+-]?\d+$/g
const INT_ANY_RE = /^[+-]\d+$/g
const INT_NONE_RE = /^[^+-]\d+$/g
const INT_POSITIVE_RE = /^[+]\d+$/g
const INT_NEGATIVE_RE = /^[-]\d+$/g

function meta(sign: SignKind): SignMeta {
// prettier-ignore
switch (sign) {
case 'some': return [INT_SOME_RE, `optionally signed integer`]
case 'any': return [INT_ANY_RE, `signed integer`]
case 'none': return [INT_NONE_RE, `unsigned integer`]
case 'positive': return [INT_POSITIVE_RE, `positively signed integer`]
case 'negative': return [INT_NEGATIVE_RE, `negatively signed integer`]
}
}

export function integer(options: IntegerOptions = {}): Parser<number> {
const sign = options.sign ?? 'none'
const radix = options.radix ?? 10

return {
parse(state: State) {
const [rexpression, expectation] = meta(sign)
const result = regexp(rexpression, expectation).parse(state)

switch (result.kind) {
case 'success': {
return success(result.state, parseInt(result.value, radix))
}

case 'failure': {
return result
}
}
}
}
}

export { integer as int }
1 change: 1 addition & 0 deletions src/parsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './internal/parsers/integer'
145 changes: 145 additions & 0 deletions tests/internal/parsers/integer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as exposed from '@lib/parsers'
import { error } from '@lib/combinators'
import { integer } from '@lib/parsers'

import { result, run, should } from '@tests/@setup/jest.helpers'

describe('internal/parsers/integer', () => {
it(`should expose 'integer' ('int')`, () => {
should.expose(exposed, 'integer', 'int')
})

describe(`integer (sign = ...) (success)`, () => {
it(`should succeed if given an unsigned integer number`, () => {
const actual = run(integer(), '42')
const expected = result('success', 42)

should.matchState(actual, expected)
})

it(`should succeed if configured to accept optionally unsigned integers (sign = some)`, () => {
const cases = [
[integer({ sign: 'some' }), '+42', result('success', 42)],
[integer({ sign: 'some' }), '-42', result('success', -42)],
[integer({ sign: 'some' }), '42', result('success', 42)]
] as const

cases.forEach(([parser, input, expected]) => {
should.matchState(run(parser, input), expected)
})
})

it(`should succeed if configured to accept signed integers (sign = any)`, () => {
const cases = [
[integer({ sign: 'any' }), '+42', result('success', 42)],
[integer({ sign: 'any' }), '-42', result('success', -42)]
] as const

cases.forEach(([parser, input, expected]) => {
should.matchState(run(parser, input), expected)
})
})

it(`should succeed if configured to not accept signed integers (sign = none)`, () => {
const cases = [
[integer({ sign: 'none' }), '42', result('success', 42)],
[integer({ sign: 'none' }), '8912493', result('success', 8912493)]
] as const

cases.forEach(([parser, input, expected]) => {
should.matchState(run(parser, input), expected)
})
})

it(`should succeed if configured to accept positively signed integers (sign = positive)`, () => {
const cases = [
[integer({ sign: 'positive' }), '+42', result('success', 42)],
[integer({ sign: 'positive' }), '+8912493', result('success', 8912493)]
] as const

cases.forEach(([parser, input, expected]) => {
should.matchState(run(parser, input), expected)
})
})

it(`should succeed if configured to accept negatively signed integers (sign = positive)`, () => {
const cases = [
[integer({ sign: 'negative' }), '-42', result('success', -42)],
[integer({ sign: 'negative' }), '-8912493', result('success', -8912493)]
] as const

cases.forEach(([parser, input, expected]) => {
should.matchState(run(parser, input), expected)
})
})
})

describe(`integer (radix = ...) (success)`, () => {
it(`should succeed with hexadecimal value if configured to use radix = 16`, () => {
const actual = run(integer({ radix: 16 }), '100')
const expected = result('success', 256)

should.matchState(actual, expected)
})

it(`should succeed with hexadecimal value if configured to use radix = 8`, () => {
const actual = run(integer({ radix: 8 }), '100')
const expected = result('success', 64)

should.matchState(actual, expected)
})

it(`should succeed with hexadecimal value if configured to use radix = 2`, () => {
const actual = run(integer({ radix: 2 }), '100')
const expected = result('success', 4)

should.matchState(actual, expected)
})
})

describe(`integer (failure)`, () => {
function withTestCases(value: string) {
const message = 'integer'

const cases = [
[error(integer(), message), `${value}`, result('failure', message)],
[error(integer({ sign: 'some' }), message), `+${value}`, result('failure', message)],
[error(integer({ sign: 'some' }), message), `-${value}`, result('failure', message)],
[error(integer({ sign: 'some' }), message), `${value}`, result('failure', message)],
[error(integer({ sign: 'any' }), message), `-${value}`, result('failure', message)],
[error(integer({ sign: 'any' }), message), `+${value}`, result('failure', message)],
[error(integer({ sign: 'none' }), message), `${value}`, result('failure', message)],
[error(integer({ sign: 'positive' }), message), `+${value}`, result('failure', message)],
[error(integer({ sign: 'negative' }), message), `-${value}`, result('failure', message)]
] as const

cases.forEach(([parser, input, expected]) => {
should.matchState(run(parser, input), expected)
})
}

it(`should fail if given a float`, () => {
withTestCases('42.00')
withTestCases('0.42')
})

it(`should fail if given an exponential`, () => {
withTestCases('0e-5')
withTestCases('2e+3')
withTestCases('1e3')
})

it(`should fail if given a binary`, () => {
withTestCases('0b100')
})

it(`should fail if given an octal`, () => {
withTestCases('0o644')
})

it(`should fail if given a hexadecimal`, () => {
withTestCases('0xd3ad')
withTestCases('0xf00d')
})
})
})

0 comments on commit f206d49

Please sign in to comment.