Skip to content

Commit

Permalink
feat: add series concatenation
Browse files Browse the repository at this point in the history
Now we can concatenate two or more series with "&". Yay!
  • Loading branch information
h-sifat committed Feb 2, 2022
1 parent 690d595 commit 3b8e5b7
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 73 deletions.
118 changes: 76 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
`Char-[ 's', 'e', 'r', 'i', 'e', 's' ]`
--------
# `Char-[ 's', 'e', 'r', 'i', 'e', 's' ]`
Just a simple function to generate an array of characters form the given range
string. For exmaple __`"0..9"`__ to generate `["0", "1", "2", ..., "9"]`
string. For example __`"0..9"`__ to generate `["0", "1", "2", ..., "9"]`

## Importing

Expand All @@ -24,53 +23,92 @@ __html__
```

## Usages
The `series` function takes 3 kinds of argument: `string`, `object` and
`tagged-template`.
The `series` function takes 4 kinds of argument: `string`, `object`,
`tagged-template`, or an `array of objects`.

```js
series `1..3` // tagged template
series("1..3") // string
series({from: "1", to: "3"}) // object
// all the above function call generates the array ["1", "2", "3"]

series([{from: "1", to: "3"}, {from: "a", to: "c"}]) // object array
// this gives us the array: ["1", "2", "3", "a", "b", "c"]
```
__Note:__ In _html_ the function name is `char_series`. So use
`char_series("1..3")`.

1. __String Range Syntax__
1. `"<from>..<to>"` gives us all characters from the `from`(inclusive)
character to the `to`(inclusive) character. e.g., `"a..e"`, `"0..9"`. We can
even get a reversed array just by reversing the start and end order. So
`"c..a"` would give us `["c", "b", "a"]`
1. `<before-count>-<char>` gives all the characters starting from
`before-count` characters before the `char` character and ending at the
`char` character. e.g., `"2-c"` gives us `["a", "b", "c"]`
1. `<char>+<after-count>` gives all the characters starting from the `char`
character and ending at `after-count` characters after the `char` character.
e.g., `"a+2"` gives us `["a", "b", "c"]`
1. `<before-count>-<char>+<after-count>` is the combination of the previous
two method. e.g., `"2-c+2"` gives us `["a", "b", "c", "d", "e"]`
1. __Flag:__ Any series can be reversed by adding a __`"!r"`__ flag at the
end. e.g., `"a..c!r"` gives us `["c", "b", "a"]` and `"c..a!r"` gives us
`["a", "b", "c"]`.

2. __Object Argument Type__
1. `{from: char; to: char}` e.g., `{from: "a", to: "c"}`
1. `{char: char; before: number}` e.g., `{char: "c", before: 2}`
1. `{char: char; after: number}` e.g., `{char: "a", after: 2}`
1. `{char: char; before: number; after: number}` e.g.,
`{char: "c", before: 2, after: 2}`
1. __Flag:__ All these objects take an optional `{reverse: boolean}`
property to reverse a series. e.g., `{from: "a", to: "c", reverse: true}`
gives us `["c", "b", "a"]` and `{from: "c", to: "a", reverse: true}` gives
us `["a", "b", "c"]`.



## Argument Types

### String Range Syntax

1. __`"<from>..<to>"`__ gives us all characters from the `from`(inclusive)
character to the `to`(inclusive) character. e.g., `"a..e"`, `"0..9"`. We can
even get a reversed array just by reversing the start and end order. So
`"c..a"` would give us `["c", "b", "a"]`
1. __`<before-count>-<char>`__ gives all the characters starting from
`before-count` characters before the `char` character and ending at the
`char` character. e.g., `"2-c"` gives us `["a", "b", "c"]`
1. __`<char>+<after-count>`__ gives all the characters starting from the `char`
character and ending at `after-count` characters after the `char` character.
e.g., `"a+2"` gives us `["a", "b", "c"]`
1. __`<before-count>-<char>+<after-count>`__ is the combination of the previous
two method. e.g., `"2-c+2"` gives us `["a", "b", "c", "d", "e"]`

__Flag:__ Any series can be reversed by adding a __`"!r"`__ flag at the
end or switching the `from` `to` characters order.
e.g., `"a..c!r"` gives us `["c", "b", "a"]` and `"c..a!r"` gives us
`["a", "b", "c"]`.

#### Series Concatenation for string argument
Two or more series can be concatenated with an __`"&"`__ character e.g.,
`"0..9&a..z"`. But this feature gives rise to a problem though! What if we want
to see the series `"&..'"`? It should return us `["&", "'"]` but it throws an
exception.

So to solve this problem we've to __escape__ the __`"&"`__ character with a
__`"/"`__ (Notice: It's not a backslash __`"\"`__) whenever it __doesn't__
represent a __delimiter__. And then we also have to __escape__
the __`"/"`__ with another __`"/"`__ character if we want to write a single
__`"/"`__ character (Ugh :unamused:)!

Examples:
Un-Escaped and Incorrect | Escaped and Correct
-------------------------|---------------------
`"&..."` | `"/&..."`
`"*../"` | `"*..//"`
`"a..&&1..5"` | `"a../&&1..5"`
`"a..&&1..5&a..c"` | `"a../&&1..5&a..c!r"`


### Object Argument Type
1. __`{from: char; to: char}`__ e.g., `{from: "a", to: "c"}`
1. __`{char: char; before: number}`__ e.g., `{char: "c", before: 2}`
1. __`{char: char; after: number}`__ e.g., `{char: "a", after: 2}`
1. __`{char: char; before: number; after: number}`__ e.g.,
`{char: "b", before: 1, after: 1}`

All the examples above gives us the array `["a", "b", "c"]`.

__Flag:__ All these objects take an optional __`{reverse: boolean}`__
property to reverse a series. e.g., `{from: "a", to: "c", reverse: true}`
gives us `["c", "b", "a"]` and `{from: "c", to: "a", reverse: true}` gives
us `["a", "b", "c"]`.


#### Series Concatenation for object argument
To concatenate tow or more series we can pass an array of series objects, e.g.,
`[{from: "1", to: "3"}, {char: "a", after: 2}]` to get the series
`["1", "2", "3", "a", "b", "c"]`.

## Examples
```js
const alphanumeric = [...series `0..9`, ...series `a..z`];
/* [
// or better if we use series concatenation
const alphanumeric = series `0..9&a..z` // or series("0..9&a..z");
// or
const alphanumeric = series([{from: "0", to: "9"}, {from: "a", to: "z"}])
/* alphanumeric = [
'0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k',
Expand All @@ -94,11 +132,7 @@ const five_chars_before_zero = series({char: "0", before: 5}) // or series `5-0`
// 5 characters before "t" and 2 characters after "t" then reverse it
const my_chars = series `5-t+2!r` // or series("5-t+2!r")
//or series({char: "t", before: 5, after: 2, reverse: true})
/* [
'v', 'u', 't',
's', 'r', 'q',
'p', 'o'
] */
/* [ 'v', 'u', 't', 's', 'r', 'q', 'p', 'o' ] */
```

## CLI
Expand Down Expand Up @@ -128,5 +162,5 @@ getSeries({from: "a".charCodeAt(0), to: "c".charCodeAt(0)})
// and we get ["a", "b", "c"]
```

Finally, let me know if there is any improvements that I can make to this
Finally, let me know if there are any improvements that I can make to this
package and kindly give this package a :star: on github if you like it :)
22 changes: 15 additions & 7 deletions cli/help-txt.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@ const helpText = `Series - A character series generator just like "A..Z" of bash
Synopsis: series [-s <separator>] character-range
Usages:
1. series "a..c" # outputs: "a b c"
2. series "a+2" # outputs 2 character after "a" (inclusive): "a b c"
3. series "2-c" # outputs 2 character before "c" (inclusive): "a b c"
4. series "2-c+2" # Combination of above two syntax, outputs: "a b c d e"
1. series "a..c" # outputs: "a b c".
2. series "a+2" # outputs 2 character after "a" (inclusive): "a b c".
3. series "2-c" # outputs 2 character before "c" (inclusive): "a b c".
4. series "2-c+2" # Combination of above two syntax, outputs: "a b c d e".
Tip: Reversing character order or adding a "!r" flag at the end reverses the
series. e.g., "c..a" => "c b a" or "a+2!r" => "c b a"
To concatenate one or more series use the "&" character as a delimiter between
tow series. e.g., "0..9&a..z". Whenever we don't use "&" as a delimiter then
we've to escape it with the "/" character. e.g., use "/&..a" instead of "&..a"
which is incorrect.
Note: As "/" is our escape flag so we also need to escape it.
e.g., use: "*..//" instead of "*../".
Reversing a series: switching character order or adding a "!r" flag at the end
reverses the series. e.g., "c..a" => "c b a" or "a+2!r" => "c b a".
Options:
[-s <separator>]: specify a separator string to join all the generated
characters. e.g., series -s "-" "a..c" outputs "a-b-c"
characters. e.g., series -s "-" "a..c" outputs "a-b-c".
[--help]: shows the help text.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "char-series",
"version": "1.0.0",
"version": "2.0.0",
"description": "Just a simple function to generate an array of characters form the given range string.",
"keywords": [
"characters",
"brace-expansion",
"range-expansion",
"character-range",
"character-series"
],
Expand Down
39 changes: 25 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import { FirstArg, FromTo } from "./interface";
import {
is,
processRangeString,
parseSeries,
normalizeRangeObject,
processTaggedTemplate,
splitConcatenatedSeries,
} from "./util";

export default function series(firstArg: FirstArg, ...values: any[]): string[] {
let rangeObject: FromTo<number>;

if (is("string")(firstArg))
rangeObject = normalizeRangeObject(processRangeString(firstArg));
else if (is("non_null_object")(firstArg)) {
if (!Array.isArray(firstArg)) rangeObject = normalizeRangeObject(firstArg);
else {
const rangeString = processTaggedTemplate(firstArg, values);
rangeObject = normalizeRangeObject(processRangeString(rangeString));
}
} else throw new Error(`Invalid argument: "${firstArg}"`);

return getSeries(rangeObject);
if (!firstArg) throw new Error(`Invalid argument`);

let rangeObjects: FromTo<number>[] = [];

if (is("string")(firstArg) || is("string[]")(firstArg)) {
const seriesString = Array.isArray(firstArg)
? processTaggedTemplate(firstArg, values)
: firstArg;

rangeObjects = splitConcatenatedSeries(seriesString)
.map(parseSeries)
.map(normalizeRangeObject);
} else if (is("non_null_object")(firstArg))
rangeObjects = is("non_null_object[]")(firstArg)
? firstArg.map(normalizeRangeObject)
: [normalizeRangeObject(firstArg)];
else throw new Error(`Invalid argument: "${firstArg}"`);

let seriesArray: string[] = [];
for (const rangeObject of rangeObjects)
seriesArray = seriesArray.concat(getSeries(rangeObject));

return seriesArray;
}

function getSeries({ from, to }: FromTo<number>) {
Expand Down
19 changes: 18 additions & 1 deletion src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ export type RangeObject =
| AfterCharRange // "e+5"
| BeforeAndAfterCharRange; // "5-e+5"

type RangeObjectsWithOptionalReverseProp = FilterKeys<
RangeObject,
"reverse"
> & { reverse?: boolean };

export type FirstArg =
| TemplateStringsArray
| (FilterKeys<RangeObject, "reverse"> & { reverse?: boolean })
| RangeObjectsWithOptionalReverseProp
| RangeObjectsWithOptionalReverseProp[]
| string;

type FilterKeys<T, KeyToRemove extends keyof T> = {
Expand All @@ -33,4 +39,15 @@ export interface AllTypes {
object: object | null;
non_null_object: object;
positive_integer: number;

// array types
"char[]": string[];
"string[]": string[];
"number[]": number[];
"symbol[]": Symbol[];
"boolean[]": boolean[];
"function[]": Function[];
"object[]": (object | null)[];
"non_null_object[]": object[];
"positive_integer[]": number[];
}
51 changes: 50 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ export function processTaggedTemplate(strings: string[], values: any[]) {
}

export function is<T extends keyof AllTypes>(type: T) {
if (type.endsWith("[]")) {
const elementType = type.slice(0, -2) as keyof AllTypes;

return (arg: any): arg is AllTypes[T] =>
Array.isArray(arg) && arg.every(is(elementType));
}

switch (type) {
case "char":
return (arg: any): arg is AllTypes[T] =>
Expand Down Expand Up @@ -69,7 +76,7 @@ const RANGE_STRING_PROCESSORS = [
}),
},
];
export function processRangeString(rangeString: string): RangeObject {
export function parseSeries(rangeString: string): RangeObject {
let result: RegExpExecArray | null;
for (const processor of RANGE_STRING_PROCESSORS)
if ((result = processor.pattern.exec(rangeString)))
Expand Down Expand Up @@ -155,3 +162,45 @@ function assertValidFromToCharRange(arg: any): asserts arg is FromToCharRange {
if (!is("char")(arg.from) || !is("char")(arg.to) || arg.to === arg.from)
throw new Error(`Invalid range object.`);
}

const DELIMITER = "&";
const ESCAPE_FLAG = "/";
export function splitConcatenatedSeries(seriesString: string): string[] {
const seriesArray: string[] = [];
seriesString += "&";

let currentSeries = "";
let wasPrevCharEscapeFlag = false;
for (let i = 0; i < seriesString.length; i++) {
const char = seriesString[i];

if (char === ESCAPE_FLAG) {
if (wasPrevCharEscapeFlag) {
currentSeries += ESCAPE_FLAG;
wasPrevCharEscapeFlag = false;
} else wasPrevCharEscapeFlag = true;
continue;
}

if (char !== DELIMITER) {
if (wasPrevCharEscapeFlag)
throw new Error(`Invalid escape sequence at index ${i - 1}.`);
currentSeries += char;
continue;
}

// now char is the DELIMITER
if (wasPrevCharEscapeFlag) {
if (i === seriesString.length - 1)
throw new Error(`Invalid escape sequence at index ${i - 1}.`);
currentSeries += DELIMITER;
wasPrevCharEscapeFlag = false;
continue;
}

seriesArray.push(currentSeries);
currentSeries = "";
}

return seriesArray;
}
5 changes: 5 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ describe("char-series", () => {
"4-e",
"2-c+2",
"1-b+3",
"a..b&c..e",
[
{ from: "a", to: "b" },
{ from: "c", to: "e" },
],
{ from: "a", to: "e" },
{ char: "a", after: 4 },
{ char: "e", before: 4 },
Expand Down
Loading

0 comments on commit 3b8e5b7

Please sign in to comment.