diff --git a/parser.ts b/parser.ts new file mode 100644 index 0000000..dcbf19d --- /dev/null +++ b/parser.ts @@ -0,0 +1,69 @@ +import { isNotEmpty, isString, isUndefined, trim } from "./deps.ts"; +import type { IntRange, RangeSpec, SuffixRange } from "./types.ts"; + +const RangeSpecifierRe = + /^(?([!#$%&'*+-.^_`|~A-Za-z0-9])+)=(?((([0-9])+-(([0-9])+)?)|(-([0-9])+))((\x20|\t)*,(\x20|\t)*((([0-9])+-(([0-9])+)?)|(-([0-9])+)))*)$/; + +export interface RangesSpecifier { + readonly rangeUnit: string; + readonly rangeSet: string; +} + +export function parseRangesSpecifier(input: string): RangesSpecifier { + const result = RangeSpecifierRe.exec(input); + + if (!result || !result.groups) throw Error(); + + const rangeUnit = result.groups.rangeUnit; + const rangeSet = result.groups.rangeSet; + + if (isUndefined(rangeUnit) || isUndefined(rangeSet)) { + throw SyntaxError(); + } + + return { rangeUnit, rangeSet }; +} + +const RangeSpecRe = + /^((?[0-9]+)-(?[0-9]+)?)$|^(-(?[0-9]+))$/; + +export function parseRangeSpec(input: string): IntRange | SuffixRange { + const result = RangeSpecRe.exec(input); + + if (!result || !result.groups) { + throw SyntaxError("syntax error"); + } + + const firstPos = result.groups.firstPos; + const lastPos = result.groups.lastPos; + const suffixLength = result.groups.suffixLength; + + if (isString(firstPos)) { + return { + firstPos: Number.parseInt(firstPos), + lastPos: lastPos ? Number.parseInt(lastPos) : undefined, + }; + } + + if (!isString(suffixLength)) { + throw SyntaxError("conflict syntax"); + } + + const suffix = Number.parseInt(suffixLength); + + if (isNaN(suffix)) throw SyntaxError(); + + return { suffixLength: suffix }; +} + +export function parseRangeSet(input: string): [RangeSpec, ...RangeSpec[]] { + const result = input.split(","); + + const ranges = result + .map(trim) + .map(parseRangeSpec); + + if (!isNotEmpty(ranges)) throw SyntaxError(); + + return ranges; +} diff --git a/parser_test.ts b/parser_test.ts new file mode 100644 index 0000000..d512fd7 --- /dev/null +++ b/parser_test.ts @@ -0,0 +1,94 @@ +import { + parseRangeSpec, + parseRangesSpecifier, + RangesSpecifier, +} from "./parser.ts"; +import { RangeSpec } from "./types.ts"; +import { assertEquals, assertThrows, describe, it } from "./_dev_deps.ts"; + +describe("parseRangesSpecifier", () => { + it("should return parsed ", () => { + const table: [string, RangesSpecifier][] = [ + ["bytes=0-100", { rangeUnit: "bytes", rangeSet: "0-100" }], + ["bytes=0-", { rangeUnit: "bytes", rangeSet: "0-" }], + ["bytes=-100", { rangeUnit: "bytes", rangeSet: "-100" }], + ["bytes=0-0,1-1", { rangeUnit: "bytes", rangeSet: "0-0,1-1" }], + ["bytes=-100,0-100", { rangeUnit: "bytes", rangeSet: "-100,0-100" }], + ["bytes=-100 , 0-100", { rangeUnit: "bytes", rangeSet: "-100 , 0-100" }], + ["bytes=-100 , -200 , 300-400", { + rangeUnit: "bytes", + rangeSet: "-100 , -200 , 300-400", + }], + ["unknown!=-1234567890", { + rangeUnit: "unknown!", + rangeSet: "-1234567890", + }], + ]; + + table.forEach(([input, expected]) => { + assertEquals(parseRangesSpecifier(input), expected); + }); + }); + + it("should throw error if the input is invalid syntax", () => { + const table: string[] = [ + "", + "a", + "abc", + "a=b", + "=", + "a=1", + "<>=1-", + "a=1.1", + "a=1.0", + "a=120 ", + " a=120", + " a=120 ", + "a1=120", + ]; + + table.forEach((input) => { + assertThrows(() => parseRangesSpecifier(input)); + }); + }); +}); + +describe("parseRangeSpec", () => { + it("should return parsed ", () => { + const table: [string, RangeSpec][] = [ + ["0-", { firstPos: 0, lastPos: undefined }], + ["0-0", { firstPos: 0, lastPos: 0 }], + ["100-100", { firstPos: 100, lastPos: 100 }], + ["100-0", { firstPos: 100, lastPos: 0 }], + ["100-0", { firstPos: 100, lastPos: 0 }], + + ["-0", { suffixLength: 0 }], + ["-100", { suffixLength: 100 }], + ]; + + table.forEach(([input, expected]) => { + assertEquals(parseRangeSpec(input), expected); + }); + }); + + it("should throw error if the input is invalid syntax", () => { + const table: string[] = [ + "", + "a", + "0", + "1", + "1.0-", + "0.1-", + "0-0.0", + "0-0.1", + "100-100,", + "100- ", + " 100-", + "-100,", + ]; + + table.forEach((input) => { + assertThrows(() => parseRangeSpec(input)); + }); + }); +});