diff --git a/src/internal/parsers/integer.ts b/src/internal/parsers/integer.ts new file mode 100644 index 0000000..cdb0eab --- /dev/null +++ b/src/internal/parsers/integer.ts @@ -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 { + 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 } diff --git a/src/parsers.ts b/src/parsers.ts new file mode 100644 index 0000000..1fbb4a5 --- /dev/null +++ b/src/parsers.ts @@ -0,0 +1 @@ +export * from './internal/parsers/integer' diff --git a/tests/internal/parsers/integer.spec.ts b/tests/internal/parsers/integer.spec.ts new file mode 100644 index 0000000..9dfab14 --- /dev/null +++ b/tests/internal/parsers/integer.spec.ts @@ -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') + }) + }) +})