@ehmpathy/uni-time 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/domain/UniDateTime.d.ts +38 -0
- package/dist/domain/UniDateTime.js +23 -0
- package/dist/domain/UniDateTime.js.map +1 -0
- package/dist/domain/UniDuration.d.ts +10 -0
- package/dist/domain/UniDuration.js +19 -0
- package/dist/domain/UniDuration.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/sleep.d.ts +2 -0
- package/dist/utils/sleep.js +7 -0
- package/dist/utils/sleep.js.map +1 -0
- package/package.json +88 -0
- package/readme.md +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Uladzimir Kasacheuski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { AsOfGlossary } from 'domain-glossaries';
|
|
2
|
+
import { PickOne } from 'type-fns';
|
|
3
|
+
declare const castInputToDate: (input: string | Date | PickOne<{
|
|
4
|
+
/**
|
|
5
|
+
* milliseconds since epoch
|
|
6
|
+
*/
|
|
7
|
+
mse: number;
|
|
8
|
+
/**
|
|
9
|
+
* a date
|
|
10
|
+
*/
|
|
11
|
+
date: Date;
|
|
12
|
+
}>) => Date;
|
|
13
|
+
/**
|
|
14
|
+
* a universally unambiguous datetime serialized as a string; yyyy-MM-ddThh:mm:ssZ
|
|
15
|
+
*/
|
|
16
|
+
export type UniDateTime = AsOfGlossary<string, 'uni-time'>;
|
|
17
|
+
export declare const asUniDateTime: (input: Parameters<typeof castInputToDate>[0]) => UniDateTime;
|
|
18
|
+
/**
|
|
19
|
+
* a universally unambiguous date serialized as a string; yyyy-MM-dd
|
|
20
|
+
*/
|
|
21
|
+
export type UniDate = AsOfGlossary<string, 'uni-time'>;
|
|
22
|
+
export declare const asUniDate: (input: Parameters<typeof castInputToDate>[0]) => UniDate;
|
|
23
|
+
/**
|
|
24
|
+
* a universally unambiguous month serialized as a string; yyyy-MM
|
|
25
|
+
*/
|
|
26
|
+
export type UniMonth = AsOfGlossary<string, 'uni-time'>;
|
|
27
|
+
/**
|
|
28
|
+
* a universally unambiguous date range
|
|
29
|
+
*
|
|
30
|
+
* note
|
|
31
|
+
* - since is inclusive (i.e., you've had freedom _since_ you gained it)
|
|
32
|
+
* - until is exclusive (i.e., you have money _until_ you run out)
|
|
33
|
+
*/
|
|
34
|
+
export type UniDateRange = AsOfGlossary<{
|
|
35
|
+
since: UniDate;
|
|
36
|
+
until: UniDate;
|
|
37
|
+
}, 'uni-time', false>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.asUniDate = exports.asUniDateTime = void 0;
|
|
4
|
+
const error_fns_1 = require("@ehmpathy/error-fns");
|
|
5
|
+
const date_fns_1 = require("date-fns");
|
|
6
|
+
const castInputToDate = (input) => {
|
|
7
|
+
if (input instanceof Date)
|
|
8
|
+
return input;
|
|
9
|
+
if (typeof input === 'string')
|
|
10
|
+
return new Date(input);
|
|
11
|
+
if (input.date)
|
|
12
|
+
return input.date;
|
|
13
|
+
if (input.mse)
|
|
14
|
+
return new Date(input.mse);
|
|
15
|
+
throw new error_fns_1.UnexpectedCodePathError('could not parse date from input', {
|
|
16
|
+
input,
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
const asUniDateTime = (input) => castInputToDate(input).toISOString();
|
|
20
|
+
exports.asUniDateTime = asUniDateTime;
|
|
21
|
+
const asUniDate = (input) => (0, date_fns_1.format)(castInputToDate(input), 'yyyy-MM-dd');
|
|
22
|
+
exports.asUniDate = asUniDate;
|
|
23
|
+
//# sourceMappingURL=UniDateTime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UniDateTime.js","sourceRoot":"","sources":["../../src/domain/UniDateTime.ts"],"names":[],"mappings":";;;AAAA,mDAA8D;AAC9D,uCAAkC;AAIlC,MAAM,eAAe,GAAG,CACtB,KAaM,EACN,EAAE;IACF,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;IACtD,IAAI,KAAK,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC;IAClC,IAAI,KAAK,CAAC,GAAG;QAAE,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,IAAI,mCAAuB,CAAC,iCAAiC,EAAE;QACnE,KAAK;KACN,CAAC,CAAC;AACL,CAAC,CAAC;AAMK,MAAM,aAAa,GAAG,CAC3B,KAA4C,EAC/B,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,WAAW,EAAiB,CAAC;AAFzD,QAAA,aAAa,iBAE4C;AAM/D,MAAM,SAAS,GAAG,CACvB,KAA4C,EACnC,EAAE,CAAC,IAAA,iBAAM,EAAC,eAAe,CAAC,KAAK,CAAC,EAAE,YAAY,CAAY,CAAC;AAFzD,QAAA,SAAS,aAEgD"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AsOfGlossary } from 'domain-glossaries';
|
|
2
|
+
import { PickOne } from 'type-fns';
|
|
3
|
+
export type UniDuration = AsOfGlossary<PickOne<{
|
|
4
|
+
days: number;
|
|
5
|
+
hours: number;
|
|
6
|
+
minutes: number;
|
|
7
|
+
seconds: number;
|
|
8
|
+
milliseconds: number;
|
|
9
|
+
}>, 'uni-time', false>;
|
|
10
|
+
export declare const toMilliseconds: (duration: UniDuration) => number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toMilliseconds = void 0;
|
|
4
|
+
const error_fns_1 = require("@ehmpathy/error-fns");
|
|
5
|
+
const toMilliseconds = (duration) => {
|
|
6
|
+
if (duration.days)
|
|
7
|
+
return duration.days * 24 * 60 * 60 * 1000;
|
|
8
|
+
if (duration.hours)
|
|
9
|
+
return duration.hours * 60 * 60 * 1000;
|
|
10
|
+
if (duration.minutes)
|
|
11
|
+
return duration.minutes * 60 * 1000;
|
|
12
|
+
if (duration.seconds)
|
|
13
|
+
return duration.seconds * 1000;
|
|
14
|
+
if (duration.milliseconds)
|
|
15
|
+
return duration.milliseconds;
|
|
16
|
+
throw new error_fns_1.UnexpectedCodePathError('unsupported duration unit to convert to milliseconds', { duration });
|
|
17
|
+
};
|
|
18
|
+
exports.toMilliseconds = toMilliseconds;
|
|
19
|
+
//# sourceMappingURL=UniDuration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UniDuration.js","sourceRoot":"","sources":["../../src/domain/UniDuration.ts"],"names":[],"mappings":";;;AAAA,mDAA8D;AAgBvD,MAAM,cAAc,GAAG,CAAC,QAAqB,EAAU,EAAE;IAC9D,IAAI,QAAQ,CAAC,IAAI;QAAE,OAAO,QAAQ,CAAC,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC9D,IAAI,QAAQ,CAAC,KAAK;QAAE,OAAO,QAAQ,CAAC,KAAK,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC3D,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,QAAQ,CAAC,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC;IAC1D,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;IACrD,IAAI,QAAQ,CAAC,YAAY;QAAE,OAAO,QAAQ,CAAC,YAAY,CAAC;IACxD,MAAM,IAAI,mCAAuB,CAC/B,sDAAsD,EACtD,EAAE,QAAQ,EAAE,CACb,CAAC;AACJ,CAAC,CAAC;AAVW,QAAA,cAAc,kBAUzB"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./domain/UniDateTime"), exports);
|
|
18
|
+
__exportStar(require("./domain/UniDuration"), exports);
|
|
19
|
+
__exportStar(require("./utils/sleep"), exports);
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,uDAAqC;AACrC,uDAAqC;AACrC,gDAA8B"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sleep = void 0;
|
|
4
|
+
const UniDuration_1 = require("../domain/UniDuration");
|
|
5
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, typeof ms === 'number' ? ms : (0, UniDuration_1.toMilliseconds)(ms)));
|
|
6
|
+
exports.sleep = sleep;
|
|
7
|
+
//# sourceMappingURL=sleep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sleep.js","sourceRoot":"","sources":["../../src/utils/sleep.ts"],"names":[],"mappings":";;;AAAA,uDAAoE;AAE7D,MAAM,KAAK,GAAG,CAAC,EAAwB,EAAiB,EAAE,CAC/D,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CACtB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAA,4BAAc,EAAC,EAAE,CAAC,CAAC,CACtE,CAAC;AAHS,QAAA,KAAK,SAGd"}
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ehmpathy/uni-time",
|
|
3
|
+
"author": "ehmpathy",
|
|
4
|
+
"description": "a glossary of universally intuitive time, date, and duration domain literals",
|
|
5
|
+
"version": "1.0.1",
|
|
6
|
+
"repository": "ehmpathy/uni-time",
|
|
7
|
+
"homepage": "https://github.com/ehmpathy/uni-time",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"time",
|
|
10
|
+
"date",
|
|
11
|
+
"datetime",
|
|
12
|
+
"UniDate",
|
|
13
|
+
"UniDateTime",
|
|
14
|
+
"UniDuration"
|
|
15
|
+
],
|
|
16
|
+
"bugs": "https://github.com/ehmpathy/uni-time/issues",
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=8.0.0"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"/dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build:ts": "tsc -p ./tsconfig.build.json",
|
|
26
|
+
"commit:with-cli": "npx cz",
|
|
27
|
+
"fix:format:prettier": "prettier --write '**/*.ts' --config ./prettier.config.js",
|
|
28
|
+
"fix:format": "npm run fix:format:prettier",
|
|
29
|
+
"fix:lint": "eslint -c ./.eslintrc.js src/**/*.ts --fix",
|
|
30
|
+
"build:clean": "rm dist/ -rf",
|
|
31
|
+
"build:compile": "tsc -p ./tsconfig.build.json",
|
|
32
|
+
"build": "npm run build:clean && npm run build:compile",
|
|
33
|
+
"test:commits": "LAST_TAG=$(git describe --tags --abbrev=0 @^ 2> /dev/null || git rev-list --max-parents=0 HEAD) && npx commitlint --from $LAST_TAG --to HEAD --verbose",
|
|
34
|
+
"test:types": "tsc -p ./tsconfig.build.json --noEmit",
|
|
35
|
+
"test:format:prettier": "prettier --parser typescript --check 'src/**/*.ts' --config ./prettier.config.js",
|
|
36
|
+
"test:format": "npm run test:format:prettier",
|
|
37
|
+
"test:lint:deps": "npx depcheck -c ./depcheckrc.yml",
|
|
38
|
+
"test:lint:eslint": "eslint -c ./.eslintrc.js src/**/*.ts",
|
|
39
|
+
"test:lint": "npm run test:lint:eslint && npm run test:lint:deps",
|
|
40
|
+
"test:unit": "jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main')",
|
|
41
|
+
"test:integration": "jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main')",
|
|
42
|
+
"test:acceptance:locally": "npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests",
|
|
43
|
+
"test": "npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally",
|
|
44
|
+
"test:acceptance": "npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests",
|
|
45
|
+
"prepush": "npm run test && npm run build",
|
|
46
|
+
"prepublish": "npm run build",
|
|
47
|
+
"preversion": "npm run prepush",
|
|
48
|
+
"postversion": "git push origin HEAD --tags --no-verify",
|
|
49
|
+
"postinstall": "[ -d .git ] && npx husky install || exit 0"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@ehmpathy/error-fns": "1.0.2",
|
|
53
|
+
"date-fns": "3.6.0",
|
|
54
|
+
"domain-glossaries": "1.0.0",
|
|
55
|
+
"type-fns": "0.8.1"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@commitlint/cli": "17.6.7",
|
|
59
|
+
"@commitlint/config-conventional": "13.1.0",
|
|
60
|
+
"@trivago/prettier-plugin-sort-imports": "2.0.4",
|
|
61
|
+
"@tsconfig/node-lts-strictest": "18.12.1",
|
|
62
|
+
"@types/jest": "29.2.4",
|
|
63
|
+
"@typescript-eslint/eslint-plugin": "7.8.0",
|
|
64
|
+
"@typescript-eslint/parser": "7.8.0",
|
|
65
|
+
"core-js": "3.26.1",
|
|
66
|
+
"cz-conventional-changelog": "3.3.0",
|
|
67
|
+
"declapract": "^0.11.2",
|
|
68
|
+
"declapract-typescript-ehmpathy": "^0.31.6",
|
|
69
|
+
"depcheck": "1.4.3",
|
|
70
|
+
"eslint": "8.56.0",
|
|
71
|
+
"eslint-config-airbnb-typescript": "18.0.0",
|
|
72
|
+
"eslint-config-prettier": "8.5.0",
|
|
73
|
+
"eslint-plugin-import": "2.26.0",
|
|
74
|
+
"eslint-plugin-prettier": "4.2.1",
|
|
75
|
+
"husky": "8.0.3",
|
|
76
|
+
"jest": "29.3.1",
|
|
77
|
+
"prettier": "2.8.1",
|
|
78
|
+
"ts-jest": "29.1.3",
|
|
79
|
+
"ts-node": "10.9.2",
|
|
80
|
+
"typescript": "5.4.5"
|
|
81
|
+
},
|
|
82
|
+
"config": {
|
|
83
|
+
"commitizen": {
|
|
84
|
+
"path": "./node_modules/cz-conventional-changelog"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"license": "MIT"
|
|
88
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# uni-time
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
A glossary of intuitive, universally unambiguous time, date, and duration domain literals.
|
|
7
|
+
|
|
8
|
+
# purpose
|
|
9
|
+
|
|
10
|
+
declare a universally unambiguous serialization format for dates, times, and datetime
|
|
11
|
+
- `UniDate` = `yyyy-MM-dd`
|
|
12
|
+
- `UniTime` = `hh:mm:ss.sss`
|
|
13
|
+
- `UniDateTime` = `yyyy-MM-ddThh:mm:ss.sssZ`
|
|
14
|
+
|
|
15
|
+
declare a universally intuitive interface for durations
|
|
16
|
+
- `UniDuration`
|
|
17
|
+
|
|
18
|
+
usecases
|
|
19
|
+
- clarify the format a date string should be in (`const since: UniDate = '2013-12-15`)
|
|
20
|
+
- cast date to universal format (`const birthday: UniDate = toUniDate(new Date())`)
|
|
21
|
+
- narrow type of strings with runtime validation (`if (!isUniDate(since)) throw new Error('wrong format')`)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install uni-time
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
# use
|
|
31
|
+
|
|
32
|
+
## Time, Date, DateTime
|
|
33
|
+
|
|
34
|
+
### declare that a date or datetime must be in the universal format
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const noon: UniTime;
|
|
38
|
+
const birthday: UniDate;
|
|
39
|
+
const occurredAt: UniDateTime;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### assure that a date or datetime is in the universal format
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const noon: UniTime = isUniTime.assure('12:00:00.000')
|
|
47
|
+
const birthday: UniDate = isUniDate.assure('2013-12-15')
|
|
48
|
+
const occurredAt: UniDateTime = isUniDateTime.assure('2013-12-15T07:21:13.555Z')
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
### cast a date or datetime into the universal format
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const birthday: UniDate = asUniDate(new Date())
|
|
56
|
+
const occurredAt: UniDateTime = asUniDateTime(new Date())
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## Duration
|
|
61
|
+
|
|
62
|
+
### declare a duration intuitively
|
|
63
|
+
```ts
|
|
64
|
+
const duration: UniDuration = { minutes: 7 }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### serialize to milliseconds
|
|
68
|
+
```ts
|
|
69
|
+
const durationInMs = toMilliseconds({ minutes: 7 })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### sleep
|
|
73
|
+
```ts
|
|
74
|
+
await sleep({ minutes: 7 })
|
|
75
|
+
```
|