Skip to content

Commit

Permalink
feat: templates!
Browse files Browse the repository at this point in the history
  • Loading branch information
adrgautier committed May 13, 2024
1 parent dcb4d61 commit 5ee19f8
Show file tree
Hide file tree
Showing 28 changed files with 1,134 additions and 160 deletions.
194 changes: 159 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,112 +2,236 @@

![typescript](https://img.shields.io/badge/written%20for-typescript-3178c6?style=flat-square) [![codecov](https://img.shields.io/codecov/c/github/adrgautier/soit?style=flat-square&token=IPTGBDRRJE)](https://codecov.io/gh/adrgautier/soit) ![prettier](https://img.shields.io/badge/code%20style-prettier-ff69b4?style=flat-square) [![npm](https://img.shields.io/npm/v/soit?style=flat-square)](https://www.npmjs.com/package/soit)

**Soit** (French for: either) is like an enhanced [Set()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) function which simplifies **type narrowing** and aims to replace TypeScript enums.
**Soit** (French for: either) is like an enhanced [Set()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) function which provides **type narrowing** and **template** <sup>`beta`</sup> utils.

## Motivation

The `enum` feature of TypeScript is not ideal. It does not provide type guards and is not iterable.
One of the main goal of TypeScript is to deal with **uncertainty**, to ensure that all possibilities have been taken into account during compilation. Sometimes the type itself can be uncertain (e.g. is it a `string` or a `number`?), but it is also common to know all possible values before runtime.

I wanted a simple lib which provides a way to [narrow type](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to a given set of values and can be iterated.
The simplest way to declare all possible values is to write a union:
```ts
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
```

> Inspired from the [enum feature of zod](https://github.com/colinhacks/zod/tree/v1#zod-enums).
Another approach is to use the enum feature of TypeScript:
```ts
enum HTTPMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE"
}
```

Whatever approach you use, you won't be able (easily) to [narrow a type down](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to this set of values or to [get an array from](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) this set of values.

I created *Soit* to be a simple lib that provide the features aforementioned.

## Declaration

A *Soit* instance can be created by passing literals (string, number or boolean) in an array to the `Soit` function.
A *Soit* instance can be created by passing an array of *literals* to the `Soit()` function.
```ts
const isWarmColor = Soit(["red", "orange"]);
```
import Soit from "soit";

You can infer the corresponding union using the `Infer` "helper" provided by the lib.
```ts
type WarmColor = Infer<typeof isWarmColor>; // infers "red" | "orange"
const isHTTPMethod = Soit(["GET", "POST", "PUT", "DELETE"]);
```

You can pass any string, number or boolean you want.
A [literal](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) is a specific string, number or boolean.
```ts
const isColdColor = Soit(["one", 1, true]);
const isOne = Soit(["1", "one", 1, true]);
```

*Soit* instances are **iterable** and can be used to create new definitions.
You can infer the corresponding type using the `Infer` generic provided by the lib.
```ts
const isColor = Soit([...isWarmColor, "green", "blue"]);
import { Infer } from "soit";

type Color = Infer<typeof isColor>; // infers "red" | "orange" | "green" | "blue"
type HTTPMethod = Infer<typeof isHTTPMethod>; // infers "GET" | "POST" | "PUT" | "DELETE"
```

## Guard
## Guard behavior

A *Soit* instance is intended to be used as a type guard:
```ts
function handleColor(color: Color) {
if(isWarmColor(color)) {
// color can be "red" | "orange"
function handleHTTPMethod(method: string) {
if(isHTTPMethod(method)) {
// method's value is "GET", "POST", "PUT" or "DELETE"
}
// color can be "blue" | "green"
throw new Error("Unknown HTTP method.");
}
```

## Array utils
## Iterable behavior

Because the *Soit* instance is **iterable**, you can access the corresponding array:
```ts
const colors = Array.from(isColor);
const HTTPMethodArray = Array.from(isHTTPMethod);
```

You may prefer this syntax:
```ts
const colors = [...isColor];
const HTTPMethodArray = [...isHTTPMethod];
```

`map` and `forEach` can be used without `Array.from()`.
## Array methods <sup>`deprecated`</sup>

A *Soit* instance gives access to two Array methods : `map` and `forEach`

```ts
isColor.forEach((color) => console.log(color));
isHTTPMethod.forEach(method => console.log(method));

const uppercaseColors = isColor.map(color => color.toUpperCase());
const lowerCaseHTTPMethodArray = isHTTPMethod.map(method => method.toLowerCase());
```

## Set utils
> `map` and `forEach` are simple shortcuts, e.g. :
> ```ts
> [...isHTTPMethod].forEach(method => console.log(method));
> ```
>
> The map method for instance can be confusing because it does not return a new *Soit* instance.
> For this reason, both methods will be removed with the next major release.
## Set methods
Set methods aim to create new *Soit* instances by adding or subtracting values from an existing instance.
> I created these methods before the new [composition methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#set_composition) where added to the **Set** object.
> This new API will certainly influence the naming of *Soit* methods in the next major release.
### `.subset([])`
You can create subsets using the `subset` method.
```ts
const isWarmColor = isColor.subset(["red", "orange"]);
const isHTTPMutationMethod = isHTTPMethod.subset(["POST", "PUT", "DELETE"]);
```
> This checks on build time that `"red"` and `"orange"` do exist in the `isColor` instance.
> This checks on build time that `"POST"`, `"PUT"` and `"DELETE"` do exist in the `isHTTPMethod` instance.
### `.extend([])`

You can extend an existing *Soit* instance using the `extend` method.
```ts
const isColor = isWarmColor.extend(["blue", "green"]);
const isHTTPMethod = isHTTPMutationMethod.extend(["GET"]);
```

### `.difference([])`

You can create a new instance without the specified values using the `difference` method.

```ts
const isColdColor = isColor.difference(["red", "orange", "yellow"]);
const isHTTPQueryMethod = isHTTPMethod.difference(["POST", "PUT", "DELETE"]);
```

> The given array don't need to be a subset and can contain values that don't exist in the initial instance.
## Template <sup>`beta`</sup>

The template feature allows mimicking the [template literal type](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#handbook-content) mechanism, but with runtime utils.
Let's take the following template literal type:

```ts
type TimeGetter = `get${"Seconds" | "Minutes" | "Hours"}`;
```

The `TimeGetter` type will only accept the following values: `"getSeconds"`, `"getMinutes"` and `"getHours"`.

Here is how you would use the template feature from *Soit*:

```ts
const isUnit = Soit(["Seconds", "Minutes", "Hours"]);
const isTimeGetter = Soit.Template("get", isUnit);
```
The `Template()` function is able to construct the corresponding template using the strings as the "static" parts and the *Soit* instances as the "dynamic" parts.

You can get the corresponding type with the usual `Infer` generic.

```ts
type TimeGetter = Infer<typeof isTimeGetter>;
```

### Guard behavior

Like a *Soit* instance, a *SoitTemplate* is intended to be used as a type guard:
```ts
if(isTimeGetter(method)) { ... }
```
The `isTimeGetter` guard will only accept the following values: `"getSeconds"`, `"getMinutes"` and `"getHours"`.

### Capture method 🪄

A *SoitTemplate* instance offers the `capture` method to retrieve the "dynamic" parts of the template from a string.

```ts
const [unit] = isTimeGetter.capture("getSeconds"); // unit === "Seconds"
```

### Iterable behavior and Array method

A *SoitTemplate* instance is iterable.

```ts
const timeGetterMethods = [...isTimeGetter]; // ["getSeconds", "getMinutes", "getHours"]
```

As with a regular *Soit* instance, you get the `map` and `forEach` shortcuts.

## Using *Soit* with other tools

### [TS-Pattern](https://github.com/gvergnaud/ts-pattern)

You can easily integrate *Soit* instances to your patterns using the [P.when](https://github.com/gvergnaud/ts-pattern?tab=readme-ov-file#pwhen-patterns) util :

```ts
import { P } from "ts-pattern";

const pattern = P.when(isHTTPMethod);
```

The inference will work as expected with TS-Pattern logic :

```ts
type Pattern = P.infer<typeof pattern>; // infers "GET" | "POST" | "PUT" | "DELETE"
```

### [Zod](https://github.com/colinhacks/zod)

You can integrate *Soit* instances to your Zod schemas using the [custom](https://zod.dev/?id=custom-schemas) util:

```ts
import * as z from "zod";
import { Infer } from "soit";

type HTTPMethod = Infer<typeof isHTTPMethod>;

const HTTPMethodSchema = z.custom<HTTPMethod>(isHTTPMethod);
```

Zod is not able to infer the type on its own, therefore you need to pass the corresponding type (inferred beforehand) in the generic.

## Troubleshoot

### `Type 'string' is not assignable to type 'never'.` ts(2345)

You are maybe trying to create a new *Soit* instance using a named array.

```ts
const warmColors = ["red", "orange"];
const isWarmColor = Soit(warmColors); // error ts(2345)
const HTTPMutationMethods = ["POST", "PUT", "DELETE"];
const isHTTPMutationMethods = Soit(HTTPMutationMethods); // error ts(2345)
```

*Soit* throw this error to prevent passing an unknown set of value (i.e. `string[]`). The solution here is to use the `as const` declaration in order to freeze the values and allow a proper type inference.

```ts
const warmColors = ["red", "orange"] as const;
const isWarmColor = Soit(warmColors);
const HTTPMutationMethods = ["POST", "PUT", "DELETE"] as const;
const isHTTPMutationMethods = Soit(HTTPMutationMethods);
```

The `Template()` function also requires freezing the values to allow a proper type inference :
```ts
const template = ['get', Soit(['Seconds', 'Minutes', 'Hours'])] as const;
const isTimeGetter = Soit.Template(...template);
```

This error can also occur if you pass a *Soit* instance directly to a template. You can use `as const` as follows:
```ts
const isTimeGetter = Soit.Template('get', Soit(['Seconds', 'Minutes', 'Hours'] as const));
```
13 changes: 6 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "soit",
"version": "2.0.0",
"version": "2.1.0",
"description": "Create a type guard from a list of literals.",
"main": "dist/index.js",
"sideEffects": false,
Expand All @@ -19,7 +19,7 @@
"build": "npm run clean && tsc",
"test": "jest --coverage && npm run compile",
"compile": "tsc -p ./tsconfig.test.json --noEmit",
"prettier": "prettier '(src|tests)/**/*.ts' --write"
"prettier": "prettier 'src/**/*.ts' --write"
},
"repository": {
"type": "git",
Expand All @@ -36,21 +36,20 @@
"license": "MIT",
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
"^.+\\.ts$": "ts-jest"
},
"testRegex": "/tests/.*.test.ts$",
"testRegex": "/src/.*.test.ts$",
"testPathIgnorePatterns": [
"/node_modules/",
"/dist/"
],
"collectCoverageFrom": [
"src/**/*.ts"
"src/**/*.ts",
"!src/**/*types.ts"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
Expand Down
16 changes: 1 addition & 15 deletions prettier.config.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,10 @@
module.exports = {
// write: true,
semi: true,
useTabs: false, // Indent lines with tabs instead of spaces.
printWidth: 80, // Specify the length of line that the printer will wrap on.
tabWidth: 2, // Specify the number of spaces per indentation-level.
singleQuote: true, // Use single quotes instead of double quotes.
/**
* Print trailing commas wherever possible.
* Valid options:
* - "none" - no trailing commas
* - "es5" - trailing commas where valid in ES5 (objects, arrays, etc)
* - "all" - trailing commas wherever possible (function arguments)
*/
trailingComma: 'es5',
/**
* Specify which parse to use.
* Valid options:
* - "flow"
* - "babylon"
*/
trailingComma: 'es5', // Print trailing commas wherever possible.
parser: 'typescript',
arrowParens: 'avoid',
};
3 changes: 3 additions & 0 deletions src/__tests__/__snapshots__/template.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SoitTemplate should test a complex template 1`] = `"0-0-0|0-0-1|0-0-2|0-0-3|0-0-4|0-0-5|0-0-6|0-0-7|0-0-8|0-0-9|0-1-0|0-1-1|0-1-2|0-1-3|0-1-4|0-1-5|0-1-6|0-1-7|0-1-8|0-1-9|0-2-0|0-2-1|0-2-2|0-2-3|0-2-4|0-2-5|0-2-6|0-2-7|0-2-8|0-2-9|0-3-0|0-3-1|0-3-2|0-3-3|0-3-4|0-3-5|0-3-6|0-3-7|0-3-8|0-3-9|0-4-0|0-4-1|0-4-2|0-4-3|0-4-4|0-4-5|0-4-6|0-4-7|0-4-8|0-4-9|0-5-0|0-5-1|0-5-2|0-5-3|0-5-4|0-5-5|0-5-6|0-5-7|0-5-8|0-5-9|0-6-0|0-6-1|0-6-2|0-6-3|0-6-4|0-6-5|0-6-6|0-6-7|0-6-8|0-6-9|0-7-0|0-7-1|0-7-2|0-7-3|0-7-4|0-7-5|0-7-6|0-7-7|0-7-8|0-7-9|0-8-0|0-8-1|0-8-2|0-8-3|0-8-4|0-8-5|0-8-6|0-8-7|0-8-8|0-8-9|0-9-0|0-9-1|0-9-2|0-9-3|0-9-4|0-9-5|0-9-6|0-9-7|0-9-8|0-9-9|1-0-0|1-0-1|1-0-2|1-0-3|1-0-4|1-0-5|1-0-6|1-0-7|1-0-8|1-0-9|1-1-0|1-1-1|1-1-2|1-1-3|1-1-4|1-1-5|1-1-6|1-1-7|1-1-8|1-1-9|1-2-0|1-2-1|1-2-2|1-2-3|1-2-4|1-2-5|1-2-6|1-2-7|1-2-8|1-2-9|1-3-0|1-3-1|1-3-2|1-3-3|1-3-4|1-3-5|1-3-6|1-3-7|1-3-8|1-3-9|1-4-0|1-4-1|1-4-2|1-4-3|1-4-4|1-4-5|1-4-6|1-4-7|1-4-8|1-4-9|1-5-0|1-5-1|1-5-2|1-5-3|1-5-4|1-5-5|1-5-6|1-5-7|1-5-8|1-5-9|1-6-0|1-6-1|1-6-2|1-6-3|1-6-4|1-6-5|1-6-6|1-6-7|1-6-8|1-6-9|1-7-0|1-7-1|1-7-2|1-7-3|1-7-4|1-7-5|1-7-6|1-7-7|1-7-8|1-7-9|1-8-0|1-8-1|1-8-2|1-8-3|1-8-4|1-8-5|1-8-6|1-8-7|1-8-8|1-8-9|1-9-0|1-9-1|1-9-2|1-9-3|1-9-4|1-9-5|1-9-6|1-9-7|1-9-8|1-9-9|2-0-0|2-0-1|2-0-2|2-0-3|2-0-4|2-0-5|2-0-6|2-0-7|2-0-8|2-0-9|2-1-0|2-1-1|2-1-2|2-1-3|2-1-4|2-1-5|2-1-6|2-1-7|2-1-8|2-1-9|2-2-0|2-2-1|2-2-2|2-2-3|2-2-4|2-2-5|2-2-6|2-2-7|2-2-8|2-2-9|2-3-0|2-3-1|2-3-2|2-3-3|2-3-4|2-3-5|2-3-6|2-3-7|2-3-8|2-3-9|2-4-0|2-4-1|2-4-2|2-4-3|2-4-4|2-4-5|2-4-6|2-4-7|2-4-8|2-4-9|2-5-0|2-5-1|2-5-2|2-5-3|2-5-4|2-5-5|2-5-6|2-5-7|2-5-8|2-5-9|2-6-0|2-6-1|2-6-2|2-6-3|2-6-4|2-6-5|2-6-6|2-6-7|2-6-8|2-6-9|2-7-0|2-7-1|2-7-2|2-7-3|2-7-4|2-7-5|2-7-6|2-7-7|2-7-8|2-7-9|2-8-0|2-8-1|2-8-2|2-8-3|2-8-4|2-8-5|2-8-6|2-8-7|2-8-8|2-8-9|2-9-0|2-9-1|2-9-2|2-9-3|2-9-4|2-9-5|2-9-6|2-9-7|2-9-8|2-9-9|3-0-0|3-0-1|3-0-2|3-0-3|3-0-4|3-0-5|3-0-6|3-0-7|3-0-8|3-0-9|3-1-0|3-1-1|3-1-2|3-1-3|3-1-4|3-1-5|3-1-6|3-1-7|3-1-8|3-1-9|3-2-0|3-2-1|3-2-2|3-2-3|3-2-4|3-2-5|3-2-6|3-2-7|3-2-8|3-2-9|3-3-0|3-3-1|3-3-2|3-3-3|3-3-4|3-3-5|3-3-6|3-3-7|3-3-8|3-3-9|3-4-0|3-4-1|3-4-2|3-4-3|3-4-4|3-4-5|3-4-6|3-4-7|3-4-8|3-4-9|3-5-0|3-5-1|3-5-2|3-5-3|3-5-4|3-5-5|3-5-6|3-5-7|3-5-8|3-5-9|3-6-0|3-6-1|3-6-2|3-6-3|3-6-4|3-6-5|3-6-6|3-6-7|3-6-8|3-6-9|3-7-0|3-7-1|3-7-2|3-7-3|3-7-4|3-7-5|3-7-6|3-7-7|3-7-8|3-7-9|3-8-0|3-8-1|3-8-2|3-8-3|3-8-4|3-8-5|3-8-6|3-8-7|3-8-8|3-8-9|3-9-0|3-9-1|3-9-2|3-9-3|3-9-4|3-9-5|3-9-6|3-9-7|3-9-8|3-9-9|4-0-0|4-0-1|4-0-2|4-0-3|4-0-4|4-0-5|4-0-6|4-0-7|4-0-8|4-0-9|4-1-0|4-1-1|4-1-2|4-1-3|4-1-4|4-1-5|4-1-6|4-1-7|4-1-8|4-1-9|4-2-0|4-2-1|4-2-2|4-2-3|4-2-4|4-2-5|4-2-6|4-2-7|4-2-8|4-2-9|4-3-0|4-3-1|4-3-2|4-3-3|4-3-4|4-3-5|4-3-6|4-3-7|4-3-8|4-3-9|4-4-0|4-4-1|4-4-2|4-4-3|4-4-4|4-4-5|4-4-6|4-4-7|4-4-8|4-4-9|4-5-0|4-5-1|4-5-2|4-5-3|4-5-4|4-5-5|4-5-6|4-5-7|4-5-8|4-5-9|4-6-0|4-6-1|4-6-2|4-6-3|4-6-4|4-6-5|4-6-6|4-6-7|4-6-8|4-6-9|4-7-0|4-7-1|4-7-2|4-7-3|4-7-4|4-7-5|4-7-6|4-7-7|4-7-8|4-7-9|4-8-0|4-8-1|4-8-2|4-8-3|4-8-4|4-8-5|4-8-6|4-8-7|4-8-8|4-8-9|4-9-0|4-9-1|4-9-2|4-9-3|4-9-4|4-9-5|4-9-6|4-9-7|4-9-8|4-9-9|5-0-0|5-0-1|5-0-2|5-0-3|5-0-4|5-0-5|5-0-6|5-0-7|5-0-8|5-0-9|5-1-0|5-1-1|5-1-2|5-1-3|5-1-4|5-1-5|5-1-6|5-1-7|5-1-8|5-1-9|5-2-0|5-2-1|5-2-2|5-2-3|5-2-4|5-2-5|5-2-6|5-2-7|5-2-8|5-2-9|5-3-0|5-3-1|5-3-2|5-3-3|5-3-4|5-3-5|5-3-6|5-3-7|5-3-8|5-3-9|5-4-0|5-4-1|5-4-2|5-4-3|5-4-4|5-4-5|5-4-6|5-4-7|5-4-8|5-4-9|5-5-0|5-5-1|5-5-2|5-5-3|5-5-4|5-5-5|5-5-6|5-5-7|5-5-8|5-5-9|5-6-0|5-6-1|5-6-2|5-6-3|5-6-4|5-6-5|5-6-6|5-6-7|5-6-8|5-6-9|5-7-0|5-7-1|5-7-2|5-7-3|5-7-4|5-7-5|5-7-6|5-7-7|5-7-8|5-7-9|5-8-0|5-8-1|5-8-2|5-8-3|5-8-4|5-8-5|5-8-6|5-8-7|5-8-8|5-8-9|5-9-0|5-9-1|5-9-2|5-9-3|5-9-4|5-9-5|5-9-6|5-9-7|5-9-8|5-9-9|6-0-0|6-0-1|6-0-2|6-0-3|6-0-4|6-0-5|6-0-6|6-0-7|6-0-8|6-0-9|6-1-0|6-1-1|6-1-2|6-1-3|6-1-4|6-1-5|6-1-6|6-1-7|6-1-8|6-1-9|6-2-0|6-2-1|6-2-2|6-2-3|6-2-4|6-2-5|6-2-6|6-2-7|6-2-8|6-2-9|6-3-0|6-3-1|6-3-2|6-3-3|6-3-4|6-3-5|6-3-6|6-3-7|6-3-8|6-3-9|6-4-0|6-4-1|6-4-2|6-4-3|6-4-4|6-4-5|6-4-6|6-4-7|6-4-8|6-4-9|6-5-0|6-5-1|6-5-2|6-5-3|6-5-4|6-5-5|6-5-6|6-5-7|6-5-8|6-5-9|6-6-0|6-6-1|6-6-2|6-6-3|6-6-4|6-6-5|6-6-6|6-6-7|6-6-8|6-6-9|6-7-0|6-7-1|6-7-2|6-7-3|6-7-4|6-7-5|6-7-6|6-7-7|6-7-8|6-7-9|6-8-0|6-8-1|6-8-2|6-8-3|6-8-4|6-8-5|6-8-6|6-8-7|6-8-8|6-8-9|6-9-0|6-9-1|6-9-2|6-9-3|6-9-4|6-9-5|6-9-6|6-9-7|6-9-8|6-9-9|7-0-0|7-0-1|7-0-2|7-0-3|7-0-4|7-0-5|7-0-6|7-0-7|7-0-8|7-0-9|7-1-0|7-1-1|7-1-2|7-1-3|7-1-4|7-1-5|7-1-6|7-1-7|7-1-8|7-1-9|7-2-0|7-2-1|7-2-2|7-2-3|7-2-4|7-2-5|7-2-6|7-2-7|7-2-8|7-2-9|7-3-0|7-3-1|7-3-2|7-3-3|7-3-4|7-3-5|7-3-6|7-3-7|7-3-8|7-3-9|7-4-0|7-4-1|7-4-2|7-4-3|7-4-4|7-4-5|7-4-6|7-4-7|7-4-8|7-4-9|7-5-0|7-5-1|7-5-2|7-5-3|7-5-4|7-5-5|7-5-6|7-5-7|7-5-8|7-5-9|7-6-0|7-6-1|7-6-2|7-6-3|7-6-4|7-6-5|7-6-6|7-6-7|7-6-8|7-6-9|7-7-0|7-7-1|7-7-2|7-7-3|7-7-4|7-7-5|7-7-6|7-7-7|7-7-8|7-7-9|7-8-0|7-8-1|7-8-2|7-8-3|7-8-4|7-8-5|7-8-6|7-8-7|7-8-8|7-8-9|7-9-0|7-9-1|7-9-2|7-9-3|7-9-4|7-9-5|7-9-6|7-9-7|7-9-8|7-9-9|8-0-0|8-0-1|8-0-2|8-0-3|8-0-4|8-0-5|8-0-6|8-0-7|8-0-8|8-0-9|8-1-0|8-1-1|8-1-2|8-1-3|8-1-4|8-1-5|8-1-6|8-1-7|8-1-8|8-1-9|8-2-0|8-2-1|8-2-2|8-2-3|8-2-4|8-2-5|8-2-6|8-2-7|8-2-8|8-2-9|8-3-0|8-3-1|8-3-2|8-3-3|8-3-4|8-3-5|8-3-6|8-3-7|8-3-8|8-3-9|8-4-0|8-4-1|8-4-2|8-4-3|8-4-4|8-4-5|8-4-6|8-4-7|8-4-8|8-4-9|8-5-0|8-5-1|8-5-2|8-5-3|8-5-4|8-5-5|8-5-6|8-5-7|8-5-8|8-5-9|8-6-0|8-6-1|8-6-2|8-6-3|8-6-4|8-6-5|8-6-6|8-6-7|8-6-8|8-6-9|8-7-0|8-7-1|8-7-2|8-7-3|8-7-4|8-7-5|8-7-6|8-7-7|8-7-8|8-7-9|8-8-0|8-8-1|8-8-2|8-8-3|8-8-4|8-8-5|8-8-6|8-8-7|8-8-8|8-8-9|8-9-0|8-9-1|8-9-2|8-9-3|8-9-4|8-9-5|8-9-6|8-9-7|8-9-8|8-9-9|9-0-0|9-0-1|9-0-2|9-0-3|9-0-4|9-0-5|9-0-6|9-0-7|9-0-8|9-0-9|9-1-0|9-1-1|9-1-2|9-1-3|9-1-4|9-1-5|9-1-6|9-1-7|9-1-8|9-1-9|9-2-0|9-2-1|9-2-2|9-2-3|9-2-4|9-2-5|9-2-6|9-2-7|9-2-8|9-2-9|9-3-0|9-3-1|9-3-2|9-3-3|9-3-4|9-3-5|9-3-6|9-3-7|9-3-8|9-3-9|9-4-0|9-4-1|9-4-2|9-4-3|9-4-4|9-4-5|9-4-6|9-4-7|9-4-8|9-4-9|9-5-0|9-5-1|9-5-2|9-5-3|9-5-4|9-5-5|9-5-6|9-5-7|9-5-8|9-5-9|9-6-0|9-6-1|9-6-2|9-6-3|9-6-4|9-6-5|9-6-6|9-6-7|9-6-8|9-6-9|9-7-0|9-7-1|9-7-2|9-7-3|9-7-4|9-7-5|9-7-6|9-7-7|9-7-8|9-7-9|9-8-0|9-8-1|9-8-2|9-8-3|9-8-4|9-8-5|9-8-6|9-8-7|9-8-8|9-8-9|9-9-0|9-9-1|9-9-2|9-9-3|9-9-4|9-9-5|9-9-6|9-9-7|9-9-8|9-9-9"`;
Loading

0 comments on commit 5ee19f8

Please sign in to comment.