@parischap/conversions 0.3.0 → 0.4.0

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.
Files changed (2) hide show
  1. package/README.md +8 -1258
  2. package/package.json +12 -3
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  # conversions
4
4
 
5
- An [`Effect`](https://effect.website/docs/introduction) library to partially replace the native javascript INTL namespace.
5
+ An [`Effect`](https://effect.website/docs/introduction) library to partially replace the native javascript INTL API.
6
6
 
7
- Non machine-dependent, safe, bidirectional (implements parsing and formatting), tested and documented, 100% Typescript, 100% functional.
7
+ Non machine-dependent, safe, bidirectional (implements parsing and formatting), tested, documented, with lots of examples, 100% Typescript, 100% functional.
8
8
 
9
9
  Can also come in handy to non-`Effect` users.
10
10
 
@@ -45,1261 +45,11 @@ After reading this introduction, you may take a look at the [API](https://parisc
45
45
 
46
46
  This package contains:
47
47
 
48
- - a [module to round numbers and BigDecimal's](#RoundingModule) with the same rounding options as those offered by the javascript INTL namespace: Ceil, Floor, Expand, Trunc, HalfCeil...
49
- - a safe, easy-to-use [number/BigDecimal parser/formatter](#NumberParserFormatter) with almost all the options offered by the javascript INTL namespace: choice of the thousand separator, of the fractional separator, of the minimum and maximum number of fractional digits, of the rounding mode, of the sign display mode, of whether to show or not the integer part when it's zero, of whether to use a scientific or engineering notation, of the character to use as exponent mark... It can also be used as a `Schema` instead of the `Effect.Schema.NumberFromString` transformer.
50
- - an equivalent to the PHP [sprintf and sscanf functions](#Templating) with real typing of the placeholders. Although `Effect.Schema` does offer the [`TemplateLiteralParser` API](https://effect.website/docs/schema/basic-usage/#templateliteralparser), the latter does not provide a solution to situations such as fixed length fields (potentially padded), numbers formatted otherwise than in the English format... This module can also be used as a `Schema`.
51
- - a very easy to use [DateTime module](#DateTimeModule) that implements natively the Iso calendar (Iso year and Iso week). It is also faster than its `Effect` counterpart as it implements an internal state that's only used to speed up calculation times (but does not alter the result of functions; so `CVDateTime` functions can be viewed as pure from a user's perspective). It can therefore be useful in applications where time is of essence.
52
- - a [DateTime parser/formatter](#DateTimeParserFormatter) which supports many of the available [unicode tokens](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table). It can also be used as a `Schema` instead of the `Effect.Schema.Date` transformer.
53
- - a few [brands](#Branding) which come in handy in many projects such as email, semantic versioning, integer numbers, positive integer numbers, real numbers and positive real numbers. All these brands are also defined as `Schemas`. Please read the [`Effect` documentation about Branding](https://effect.website/docs/code-style/branded-types/) if you are not familiar with this concept
48
+ - a [module to round numbers and BigDecimal's](./readme-assets/Rounding.md) with the same rounding options as those offered by the javascript INTL namespace: Ceil, Floor, Expand, Trunc, HalfCeil...
49
+ - a safe, easy-to-use [number/BigDecimal parser/formatter](./readme-assets/NumberParserFormatter.md) with almost all the options offered by the javascript INTL namespace: choice of the thousand separator, of the fractional separator, of the minimum and maximum number of fractional digits, of the rounding mode, of the sign display mode, of whether to show or not the integer part when it's zero, of whether to use a scientific or engineering notation, of the character to use as exponent mark... It can also be used as a `Schema` instead of the `Effect.Schema.NumberFromString` transformer.
50
+ - an equivalent to the PHP [sprintf and sscanf functions](./readme-assets/Templating.md) with real typing of the placeholders. Although `Effect.Schema` does offer the [`TemplateLiteralParser` API](https://effect.website/docs/schema/basic-usage/#templateliteralparser), the latter does not provide a solution to situations such as fixed length fields (potentially padded), numbers formatted otherwise than in the English format... This module can also be used as a `Schema`.
51
+ - a very easy to use [DateTime module](./readme-assets/DateTime.md) that implements natively the Iso calendar (Iso year and Iso week). It is also faster than its `Effect` counterpart as it implements an internal state that's only used to speed up calculation times (but does not alter the result of functions; so `CVDateTime` functions can be viewed as pure from a user's perspective). It can therefore be useful in applications where time is of essence.
52
+ - a [DateTime parser/formatter](./readme-assets/DateTimeFormatter.md) which supports many of the available [unicode tokens](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table). It can also be used as a `Schema` instead of the `Effect.Schema.Date` transformer.
53
+ - a few [brands](./readme-assets/Branding.md) which come in handy in many projects such as email, semantic versioning, integer numbers, positive integer numbers, real numbers and positive real numbers. All these brands are also defined as `Schemas`. Please read the [`Effect` documentation about Branding](https://effect.website/docs/code-style/branded-types/) if you are not familiar with this concept
54
54
 
55
55
  Most functions of this package return an `Either` or an `Option` to signify the possibility of an error. However, if you are not an `Effect` user and do not care to learn more about it, you can simply use the `OrThrow` variant of the function. For instance, use `CVDateTime.setWeekdayOrThrow` instead of `CVDateTime.setWeekday`. As its name suggests, it will throw in case failure. Some functions return functions that return an `Either` or throw. In that case, the variant for non-`Effect` users contains the word `Throwing`, e.g. use `CVDateTimeFormat.toThrowingFormatter` instead of `CVDateTimeFormat.toFormatter`.
56
-
57
- ### <a id="RoundingModule"></a>A) Rounding module
58
-
59
- #### 1. Usage example
60
-
61
- ```ts
62
- import { CVRoundingMode, CVRoundingOption } from "@parischap/conversions";
63
- import { BigDecimal } from "effect";
64
-
65
- // Here we define our rounding options:
66
- // the result must have three fractional digits using the HalfEven rounding mode
67
- const roundingOption = CVRoundingOption.make({
68
- precision: 3,
69
- roundingMode: CVRoundingMode.Type.HalfEven,
70
- });
71
-
72
- // Let's define a number rounder from our options. Type: (value:number) => number
73
- const numberRounder = CVRoundingOption.toNumberRounder(roundingOption);
74
- // Let's define a BigDecimal rounder from our options. Type: (value:BigDecimal) => BigDecimal
75
- const bigDecimalRounder = CVRoundingOption.toBigDecimalRounder(roundingOption);
76
-
77
- /** Positive numbers with even last significant digit */
78
- // Result: 12.457
79
- console.log(numberRounder(12.4566));
80
-
81
- // Result: 12.456
82
- console.log(numberRounder(12.4565));
83
-
84
- // Result: 12.456
85
- console.log(numberRounder(12.4564));
86
-
87
- /** Positive numbers with odd last significant digit */
88
- // Result: 12.458
89
- console.log(numberRounder(12.4576));
90
-
91
- // Result: 12.458
92
- console.log(numberRounder(12.4575));
93
-
94
- // Result: 12.457
95
- console.log(numberRounder(12.4574));
96
-
97
- /** Negative numbers with even last significant digit */
98
- // Result: -12.457
99
- console.log(numberRounder(-12.4566));
100
-
101
- // Result: -12.456
102
- console.log(numberRounder(-12.4565));
103
-
104
- // Result: -12.456
105
- console.log(numberRounder(-12.4564));
106
-
107
- /** Negative numbers with odd last significant digit */
108
- // Result: -12.458
109
- console.log(numberRounder(-12.4576));
110
-
111
- // Result: -12.458
112
- console.log(numberRounder(-12.4575));
113
-
114
- // Result: -12.457
115
- console.log(numberRounder(-12.4574));
116
-
117
- // Result: -12.450000000000001 (javascript number loss of accuracy)
118
- console.log(numberRounder(-12.45));
119
-
120
- /** Diverse BigDecimal numbers */
121
- // Result: BigDecimal.make(12457n, 3)
122
- console.log(bigDecimalRounder(BigDecimal.make(124566n, 4)));
123
-
124
- // Result: BigDecimal.make(-12456n, 3)
125
- console.log(bigDecimalRounder(BigDecimal.make(-124565n, 4)));
126
-
127
- // Result: BigDecimal.make(12450n, 3)
128
- console.log(bigDecimalRounder(BigDecimal.make(1245n, 2)));
129
- ```
130
-
131
- ### 2. Available rounding modes
132
-
133
- The available rounding modes are defined in module RoundingMode.ts:
134
-
135
- ```ts
136
- export enum Type {
137
- /** Round toward +∞. Positive values round up. Negative values round "more positive" */
138
- Ceil = 0,
139
- /** Round toward -∞. Positive values round down. Negative values round "more negative" */
140
- Floor = 1,
141
- /**
142
- * Round away from 0. The magnitude of the value is always increased by rounding. Positive values
143
- * round up. Negative values round "more negative"
144
- */
145
- Expand = 2,
146
- /**
147
- * Round toward 0. The magnitude of the value is always reduced by rounding. Positive values round
148
- * down. Negative values round "less negative"
149
- */
150
- Trunc = 3,
151
- /**
152
- * Ties toward +∞. Values above the half-increment round like "ceil" (towards +∞), and below like
153
- * "floor" (towards -∞). On the half-increment, values round like "ceil"
154
- */
155
- HalfCeil = 4,
156
- /**
157
- * Ties toward -∞. Values above the half-increment round like "ceil" (towards +∞), and below like
158
- * "floor" (towards -∞). On the half-increment, values round like "floor"
159
- */
160
- HalfFloor = 5,
161
- /**
162
- * Ties away from 0. Values above the half-increment round like "expand" (away from zero), and
163
- * below like "trunc" (towards 0). On the half-increment, values round like "expand"
164
- */
165
- HalfExpand = 6,
166
- /**
167
- * Ties toward 0. Values above the half-increment round like "expand" (away from zero), and below
168
- * like "trunc" (towards 0). On the half-increment, values round like "trunc"
169
- */
170
- HalfTrunc = 7,
171
- /**
172
- * Ties towards the nearest even integer. Values above the half-increment round like "expand"
173
- * (away from zero), and below like "trunc" (towards 0). On the half-increment values round
174
- * towards the nearest even digit
175
- */
176
- HalfEven = 8,
177
- }
178
- ```
179
-
180
- #### 3. `CVRoundingOption` instances
181
-
182
- Instead of building your own `CVRoundingOption`, you can use the `halfExpand2` `CVRoundingOption` instance (`HalfExpand` rounding mode with a precision of two fractional digits). It will come in handy in accounting apps of most countries. For example:
183
-
184
- ```ts
185
- import { CVRoundingOption } from "@parischap/conversions";
186
-
187
- // Let's define a number rounder from halfExpand2. Type: (value:number) => number
188
- const numberRounder = CVRoundingOption.toNumberRounder(
189
- CVRoundingOption.halfExpand2,
190
- );
191
-
192
- /** Positive number */
193
- // Result: 12.456
194
- console.log(numberRounder(12.46));
195
-
196
- /** Negative number */
197
- // Result: -12.46
198
- console.log(numberRounder(-12.457));
199
- ```
200
-
201
- #### 4. Debugging and equality
202
-
203
- `CVRoundingOption` objects implement `Effect` equivalence and equality based on equivalence and equality of the `precision` and `roundingMode` properties. They also implement a `.toString()` method. For instance:
204
-
205
- ```ts
206
- import { CVRoundingMode, CVRoundingOption } from "@parischap/conversions";
207
- import { Equal } from "effect";
208
-
209
- // Result: 'HalfExpandRounderWith2Precision'
210
- console.log(CVRoundingOption.halfExpand2);
211
-
212
- const dummyOption1 = CVRoundingOption.make({
213
- precision: 3,
214
- roundingMode: CVRoundingMode.Type.HalfEven,
215
- });
216
-
217
- const dummyOption2 = CVRoundingOption.make({
218
- precision: 2,
219
- roundingMode: CVRoundingMode.Type.HalfExpand,
220
- });
221
-
222
- // Result: false
223
- console.log(Equal.equals(CVRoundingOption.halfExpand2, dummyOption1));
224
-
225
- // Result: true
226
- console.log(Equal.equals(CVRoundingOption.halfExpand2, dummyOption2));
227
- ```
228
-
229
- ### <a id="NumberParserFormatter"></a>B) Number and BigDecimal parser/formatter
230
-
231
- #### 1. Usage example
232
-
233
- ```ts
234
- import { CVNumberBase10Format, CVReal, CVSchema } from "@parischap/conversions";
235
- import { pipe, Schema } from "effect";
236
-
237
- // Let's define some formats
238
- const ukStyleUngroupedNumber = CVNumberBase10Format.ukStyleUngroupedNumber;
239
- const ukStyleNumberWithEngineeringNotation = pipe(
240
- CVNumberBase10Format.ukStyleNumber,
241
- CVNumberBase10Format.withEngineeringScientificNotation,
242
- );
243
-
244
- const frenchStyleInteger = CVNumberBase10Format.frenchStyleInteger;
245
-
246
- // Let's define a formatter
247
- // Type: (value: BigDecimal | CVReal.Type) => string
248
- const ukStyleWithEngineeringNotationFormatter =
249
- CVNumberBase10Format.toNumberFormatter(ukStyleNumberWithEngineeringNotation);
250
-
251
- // Let's define a parser
252
- // Type: (value: string ) => Option.Option<CVReal.Type>
253
- const ungroupedUkStyleParser = CVNumberBase10Format.toRealParser(
254
- ukStyleUngroupedNumber,
255
- );
256
-
257
- // Let's define a parser that throws for non Effect users
258
- // Type: (value: string ) => CVReal.Type
259
- const throwingParser = CVNumberBase10Format.toThrowingRealParser(
260
- ukStyleUngroupedNumber,
261
- );
262
-
263
- // Result: '10.341e3'
264
- console.log(
265
- ukStyleWithEngineeringNotationFormatter(CVReal.unsafeFromNumber(10340.548)),
266
- );
267
-
268
- // result: { _id: 'Option', _tag: 'Some', value: 10340.548 }
269
- console.log(ungroupedUkStyleParser("10340.548"));
270
-
271
- // result: { _id: 'Option', _tag: 'None' }
272
- console.log(ungroupedUkStyleParser("10,340.548"));
273
-
274
- // result: 10340.548
275
- console.log(throwingParser("10340.548"));
276
-
277
- // Using Schema
278
- const schema = CVSchema.Real(frenchStyleInteger);
279
-
280
- // Type: (value: string ) => Either.Either<CVReal.Type,ParseError>
281
- const frenchStyleDecoder = Schema.decodeEither(schema);
282
-
283
- // Type: (value: CVReal.Type ) => Either.Either<string,ParseError>
284
- const frenchStyleEncoder = Schema.encodeEither(schema);
285
-
286
- // Result: { _id: 'Either', _tag: 'Right', right: 1024 }
287
- console.log(frenchStyleDecoder("1 024"));
288
-
289
- // Error: Failed to convert string to a(n) potentially signed French-style integer
290
- console.log(frenchStyleDecoder("1 024,56"));
291
-
292
- // Result: { _id: 'Either', _tag: 'Right', right: '1 025' }
293
- console.log(frenchStyleEncoder(CVReal.unsafeFromNumber(1024.56)));
294
- ```
295
-
296
- #### 2. CVNumberBase10Format instances
297
-
298
- In the previous example, we used the `ukStyleNumber`, `ukStyleUngroupedNumber` and `frenchStyleInteger` `CVNumberBase10Format` instances.
299
-
300
- You will find in the [API](https://parischap.github.io/effect-libs/conversions/NumberBase10Format.ts) the list of all pre-defined instances.
301
-
302
- #### 3. CVNumberBase10Format Instance modifiers
303
-
304
- Sometimes, you will need to bring some small modifications to a pre-defined `CVNumberBase10Format` instance. For instance, in the previous example, we defined the `ukStyleNumberWithEngineeringNotation` instance by using the `withEngineeringScientificNotation` modifier on the `ukStyleNumber` pre-defined instance.
305
-
306
- There are quite a few such modifiers whose list you will find in the [API](https://parischap.github.io/effect-libs/conversions/NumberBase10Format.ts).
307
-
308
- #### 4. CVNumberBase10Format in more details
309
-
310
- If you have very specific needs, you can define your own CVNumberBase10Format instance that must comply with the following interface:
311
-
312
- ```ts
313
- export interface Type {
314
- /**
315
- * Thousand separator. Use an empty string for no separator. Usually a string made of at most one
316
- * character different from `fractionalSeparator`. Will not throw otherwise but unexpected results
317
- * might occur.
318
- */
319
- readonly thousandSeparator: string;
320
-
321
- /**
322
- * Fractional separator. Usually a one-character string different from `thousandSeparator`. Will
323
- * not throw otherwise but unexpected results might occur.
324
- */
325
- readonly fractionalSeparator: string;
326
-
327
- /**
328
- * Formatting:
329
- *
330
- * - If `true`, numbers with a null integer part are displayed starting with `0`. Otherwise, they
331
- * are displayed starting with `.` unless `maximumFractionalDigits===0`, in which case they are
332
- * displayed starting wiyh `0`.
333
- *
334
- * Parsing
335
- *
336
- * - If `true`, conversion will fail for numbers starting with `.` (after an optional sign).
337
- * - If `false`, conversion will fail for numbers starting with `0.` (after an optional sign).
338
- */
339
- readonly showNullIntegerPart: boolean;
340
-
341
- /**
342
- * Minimim number of digits forming the fractional part of a number. Must be a positive integer
343
- * (>=0) less than or equal to `maximumFractionalDigits`.
344
- *
345
- * Formatting: the string will be right-padded with `0`'s if necessary to respect the condition
346
- *
347
- * Parsing: will fail if the input string does not respect this condition (the string must be
348
- * right-padded with `0`'s to respect the condition if necessary).
349
- */
350
- readonly minimumFractionalDigits: number;
351
-
352
- /**
353
- * Maximum number of digits forming the fractional part of a number. Must be an integer value
354
- * greater than or equal to `minimumFractionalDigits`. Can take the +Infinity value.
355
- *
356
- * Formatting: the number will be rounded using the roundingMode to respect the condition (unless
357
- * `maximumFractionalDigits` is `+Infinity`).
358
- *
359
- * Parsing: will fail if the input string has too many fractional digits.
360
- */
361
- readonly maximumFractionalDigits: number;
362
-
363
- /**
364
- * Possible characters to use to represent e-notation. Usually ['e','E']. Must be an array of
365
- * one-character strings. Will not throw otherwise but unexpected results will occur. Not used if
366
- * `scientificNotation === None`
367
- *
368
- * Formatting: the string at index 0 is used
369
- *
370
- * Parsing: the first character of the e-notation must be one of the one-character strings present
371
- * in the array
372
- */
373
- readonly eNotationChars: ReadonlyArray<string>;
374
-
375
- /** Scientific notation options. See ScientificNotation */
376
- readonly scientificNotation: ScientificNotation;
377
-
378
- /** Rounding mode options. See RoundingMode.ts */
379
- readonly roundingMode: CVRoundingMode.Type;
380
-
381
- /** Sign display options. See SignDisplay.ts */
382
- readonly signDisplay: SignDisplay;
383
- }
384
- ```
385
-
386
- To build such an instance, you will need to use the `make` constructor. For instance, this is how you could redefine the `frenchStyleNumber` instance:
387
-
388
- ```ts
389
- const frenchStyleNumber = CVNumberBase10Format.make({
390
- thousandSeparator: " ",
391
- fractionalSeparator: ",",
392
- showNullIntegerPart: true,
393
- minimumFractionalDigits: 0,
394
- maximumFractionalDigits: 3,
395
- eNotationChars: ["e", "E"],
396
- scientificNotation: ScientificNotation.None,
397
- roundingMode: CVRoundingMode.Type.HalfExpand,
398
- signDisplay: SignDisplay.Negative,
399
- });
400
- ```
401
-
402
- #### 5. Debugging and equality
403
-
404
- `CVNumberBase10Format` objects implement a `.toString()` method and a `toDescription` destructor.
405
- The `.toString()` method will display the name of the object and all available properties. The `toDescription` destructor will produce a short summary of the format.
406
-
407
- For instance:
408
-
409
- ```ts
410
- import { CVNumberBase10Format } from "@parischap/conversions";
411
- import { pipe } from "effect";
412
-
413
- // Result:
414
- // {
415
- // _id: '@parischap/conversions/NumberBase10Format/',
416
- // thousandSeparator: '',
417
- // fractionalSeparator: '.',
418
- // showNullIntegerPart: true,
419
- // minimumFractionalDigits: 0,
420
- // maximumFractionalDigits: 3,
421
- // eNotationChars: [ 'e', 'E' ],
422
- // scientificNotation: 0,
423
- // roundingMode: 6,
424
- // signDisplay: 3
425
- // }
426
- console.log(CVNumberBase10Format.ukStyleUngroupedNumber);
427
-
428
- // Result: 'signed integer'
429
- console.log(
430
- pipe(
431
- CVNumberBase10Format.ukStyleUngroupedNumber,
432
- CVNumberBase10Format.withSignDisplay,
433
- CVNumberBase10Format.withNDecimals(0),
434
- CVNumberBase10Format.toDescription,
435
- ),
436
- );
437
- ```
438
-
439
- ### <a id="Templating"></a>C) Templating
440
-
441
- #### 1. Usage example
442
-
443
- ```ts
444
- /* eslint-disable functional/no-expression-statements */
445
- import {
446
- CVNumberBase10Format,
447
- CVReal,
448
- CVSchema,
449
- CVTemplate,
450
- CVTemplatePlaceholder,
451
- CVTemplateSeparator,
452
- } from "@parischap/conversions";
453
- import { MRegExpString } from "@parischap/effect-lib";
454
- import { pipe, Schema } from "effect";
455
-
456
- // Let's define useful shortcuts
457
- const ph = CVTemplatePlaceholder;
458
- const sep = CVTemplateSeparator;
459
-
460
- // Let's define a template: "#name is a #age-year old #kind."
461
- const template = CVTemplate.make(
462
- // field named 'name' that must be a non-empty string containing no space characters
463
- ph.anythingBut({ name: "name", forbiddenChars: [MRegExpString.space] }),
464
- // Immutable text
465
- sep.make(" is a "),
466
- // Field named 'age' that must represent an unsigned integer
467
- ph.real({
468
- name: "age",
469
- numberBase10Format: pipe(
470
- CVNumberBase10Format.integer,
471
- CVNumberBase10Format.withoutSignDisplay,
472
- ),
473
- }),
474
- // Immutable text
475
- sep.make("-year old "),
476
- // field named 'kind' that must be a non-empty string containing no dot character
477
- ph.anythingBut({ name: "kind", forbiddenChars: ["."] }),
478
- // Immutable text
479
- sep.dot,
480
- );
481
-
482
- // Let's define a parser. See how the return type matches the names and types of the placeholders
483
- // Type: (value: string) => Either.Either<{
484
- // readonly name: string;
485
- // readonly age: CVReal.Type;
486
- // readonly kind: string;
487
- // }, MInputError.Type>
488
- const parser = CVTemplate.toParser(template);
489
-
490
- // Let's define a parser that throws for Effect users.
491
- // Type: (value: string) => {
492
- // readonly name: string;
493
- // readonly age: CVReal.Type;
494
- // readonly kind: string;
495
- // }
496
- const throwingParser = CVTemplate.toThrowingParser(template);
497
-
498
- // Let's define a formatter.
499
- // Type: (value: {
500
- // readonly name: string;
501
- // readonly age: CVReal.Type;
502
- // readonly kind: string;
503
- // }) => Either.Either<string, MInputError.Type>
504
- const formatter = CVTemplate.toFormatter(template);
505
-
506
- // Let's define a formatter that throws for Effect users.
507
- // Type: (value: {
508
- // readonly name: string;
509
- // readonly age: CVReal.Type;
510
- // readonly kind: string;
511
- // }) => string, MInputError.Type
512
- const throwingFormatter = CVTemplate.toThrowingFormatter(template);
513
-
514
- // Result: {
515
- // _id: 'Either',
516
- // _tag: 'Left',
517
- // left: {
518
- // message: "Expected remaining text for separator at position 2 to start with ' is a '. Actual: ''",
519
- // _tag: '@parischap/effect-lib/InputError/'
520
- // }
521
- // }
522
- console.log(parser("John"));
523
-
524
- // Result: { _id: 'Either', _tag: 'Right', right: { name: 'John', age: 47, kind: 'man' } }
525
- console.log(parser("John is a 47-year old man."));
526
-
527
- // Result: { name: 'John', age: 47, kind: 'man' }
528
- console.log(throwingParser("John is a 47-year old man."));
529
-
530
- // Result: { _id: 'Either', _tag: 'Right', right: 'Tom is a 15-year old boy.' }
531
- console.log(
532
- formatter({
533
- name: "Tom",
534
- age: CVReal.unsafeFromNumber(15),
535
- kind: "boy",
536
- }),
537
- );
538
-
539
- // Result: 'Tom is a 15-year old boy.'
540
- console.log(
541
- throwingFormatter({
542
- name: "Tom",
543
- age: CVReal.unsafeFromNumber(15),
544
- kind: "boy",
545
- }),
546
- );
547
-
548
- // Using Schema
549
- const schema = CVSchema.Template(template);
550
-
551
- // Type:(i: string) => Either<{
552
- // readonly name: string;
553
- // readonly age: CVReal.Type;
554
- // readonly kind: string;
555
- // }, ParseError>
556
- const decoder = Schema.decodeEither(schema);
557
-
558
- // Type: (a: {
559
- // readonly name: string;
560
- // readonly age: CVReal.Type;
561
- // readonly kind: string;
562
- // }) => Either<string, ParseError>
563
- const encoder = Schema.encodeEither(schema);
564
-
565
- // Result: { _id: 'Either', _tag: 'Right', right: { name: 'John', age: 47, kind: 'man' } }
566
- console.log(decoder("John is a 47-year old man."));
567
-
568
- // Result: { _id: 'Either', _tag: 'Right', right: 'Tom is a 15-year old boy.' }
569
- console.log(
570
- encoder({
571
- name: "Tom",
572
- age: CVReal.unsafeFromNumber(15),
573
- kind: "boy",
574
- }),
575
- );
576
- ```
577
-
578
- #### 2. Definitions
579
-
580
- A template is a model of a text that has always the same structure. In such a text, there are immutable and mutable parts. Let's take the following two texts as an example:
581
-
582
- - text1 = "John is a 47-year old man."
583
- - text2 = "Jehnny is a 5-year old girl."
584
-
585
- These two texts obviously share the same structure which is the template:
586
-
587
- Placeholder1 is a Placeholder2-year old Placeholder3.
588
-
589
- Placeholder1, Placeholder2 and Placeholder3 are the mutable parts of the template. We call them `CVTemplatePlaceholder`'s.
590
-
591
- " is a ", "-year old " and "." are the immutable parts of the template. We call them `CVTemplateSeperator`'s.
592
-
593
- From a text with the above structure, we can extract the values of Placeholder1, Placeholder2, and Placeholder3. In the present case:
594
-
595
- - For text1: { Placeholder1 : 'John', Placeholder2 : '47', Placeholder3 : 'man' }
596
- - For text2: { Placeholder1 : 'Jehnny', Placeholder2 : '5', Placeholder3 : 'girl'}
597
-
598
- Extracting the values of placeholders from a text according to a template is called parsing. The result of parsing is an object whose properties are named after the name of the placeholders they represent.
599
-
600
- Inversely, given a template and the values of the placeholders that compose it (provided as the properties of an object), we can generate a text. This is called formatting. In the present case, with the object:
601
-
602
- { Placeholder1 : 'Tom', Placeholder2 : '15', Placeholder3 : 'boy' }
603
-
604
- we will obtain the text: "Tom is a 15-year old boy."
605
-
606
- #### 3. CVTemplateSeparator's
607
-
608
- A `CVTemplateSeparator` represents the immutable part of a template. Upon parsing, we must check that it is present as is in the text. Upon formatting, it must be inserted as is into the text.
609
-
610
- To create a `CVTemplateSeparator`, you usually call the `CVTemplateSeparator.make` constructor. However, the TemplateSeparator.ts module exports a series of predefined `CVTemplateSeparator` instances, such as `CVTemplateSeparator.slash` and `CVTemplateSeparator.space`. You can find the list of all predefined `CVTemplateSeparator` instances in the [API](https://parischap.github.io/effect-libs/conversions/TemplatePart.ts).
611
-
612
- #### 4. CVTemplatePlaceholder's
613
-
614
- A `CVTemplatePlaceholder` represents the mutable part of a template. Each `CVTemplatePlaceholder` defines a parser and a formatter:
615
-
616
- - the parser takes a text, consumes a part of that text, optionnally converts the consumed part to a value of type T and, if successful, returns a `Right` of that value and of what has not been consumed. In case of failure, it returns a `Left`.
617
- - the formatter takes a value of type T, converts it to a string (if T is not string), checks that the result is coherent and, if so, inserts that string into the text. Otherwise, it returns a `Left`.
618
-
619
- There are several predefined Placeholder's:
620
-
621
- - `fixedLength`: this Placeholder always reads/writes the same number of characters from/into the text.
622
- - `paddedFixedLength`: same as `fixedLength` but the consumed text is trimmed off of a `fillChar` on the left or right and the written text is padded with a `fillChar` on the left or right.
623
- - `fixedLengthToReal`: same as `fixedLength` but the parser tries to convert the consumed text into a `CVReal` using the passed `CVNumberBase10Format`. The formatter takes a `CVReal` and tries to convert and write it as an n-character string. You can pass a `fillChar` that is trimmed off the consumed text upon parsing and padded to the written text upon formatting.
624
- - `real`: the parser of this Placeholder reads from the text all the characters that it can interpret as a number in the provided `CVNumberBase10Format` and converts the consumed text into a `CVReal`. The formatter takes a `CVReal` and converts it into a string according to the provided `CVNumberBase10Format`.
625
- - `mappedLiterals`: this Placeholder takes as input a map that must define a bijection between a list of strings and a list of values. The parser tries to read from the text one of the strings in the list. Upon success, it returns the corresponding value. The formatter takes a value and tries to find it in the list. Upon success, it writes the corresponding string into the text.
626
- - `realMappedLiterals`: same as `mappedLiterals` but values are assumed to be of type `CVReal` which is the most usual use case.
627
- - `fulfilling`: the parser of this Placeholder reads as much of the text as it can that fulfills the passed regular expression. The formatter only accepts a string that matches the passed regular expression and writes it into the text.
628
- - `anythingBut`: this is a special case of the `fulfilling` `CVTemplatePlaceholder`. The parser reads from the text until it meets one of the `forbiddenChars` passed as parameter (the result must be a non-empty string). The formatter will only accept a non-empty string that does not contain any of the forbidden chars and write it to the text.
629
- - `toEnd`: this is another special case of the `fulfilling` `CVTemplatePlaceholder`. The parser reads all the remaining text. The formatter accepts any string and writes it. This `CVTemplatePlaceholder` should only be used as the last `CVTemplatePart` of a `CVTemplate`.
630
-
631
- Each `CVTemplatePlaceholder` must be given a name that will be used as the name of the property of the result object of parsing or of the input object of formatting. This name needs not be unique inside a CVTemplate. The same name can appear several times. However, even if there are several `CVTemplatePlaceholder`'s with the same name, there will be only one property with that name. When parsing, this implies that all `CVTemplatePlaceholder`'s with the same name must yield the same value. When formatting, this implies that the value needs only be provided once and will be shared by all `CVTemplatePlaceholder`'s with that name.
632
-
633
- If none of these `CVTemplatePlaceholder` instances suits you, you can define you own with the `make` constructor. You will find detailed explanations of the predefined `CVTemplatePlaceholder` instances and of the make constructor in the [API](https://parischap.github.io/effect-libs/conversions/TemplatePart.ts).
634
-
635
- #### 5. A more complex example
636
-
637
- ```ts
638
- import {
639
- CVNumberBase10Format,
640
- CVReal,
641
- CVTemplate,
642
- CVTemplatePlaceholder,
643
- CVTemplateSeparator,
644
- } from "@parischap/conversions";
645
-
646
- // Let's define useful shortcuts
647
- const placeholder = CVTemplatePlaceholder;
648
- const sep = CVTemplateSeparator;
649
-
650
- // Let's define a date template that will look like: 'Today is #weekday, day number #weekday of the week.'
651
- // Note that weekDay appears twice, once as a realMappedLiterals placeholder, once as a real placeholder.
652
- const template = CVTemplate.make(
653
- // Separator
654
- sep.make("Today is "),
655
- // realMappedLiterals placeHolder
656
- placeholder.realMappedLiterals({
657
- name: "weekday",
658
- keyValuePairs: [
659
- ["Monday", CVReal.unsafeFromNumber(1)],
660
- ["Tuesday", CVReal.unsafeFromNumber(2)],
661
- ["Wednesday", CVReal.unsafeFromNumber(3)],
662
- ["Thursday", CVReal.unsafeFromNumber(4)],
663
- ["Friday", CVReal.unsafeFromNumber(5)],
664
- ["Saturday", CVReal.unsafeFromNumber(6)],
665
- ["Sunday", CVReal.unsafeFromNumber(7)],
666
- ],
667
- }),
668
- // Separator
669
- sep.make(", day number "),
670
- // Field named 'weekday' that must represent an integer
671
- placeholder.real({
672
- name: "weekday",
673
- numberBase10Format: CVNumberBase10Format.integer,
674
- }),
675
- // Separator
676
- sep.make(" of the week."),
677
- );
678
-
679
- // Let's define a parser. Note that there is only one `weekday` property
680
- // Type: (value: string) => Either.Either<{
681
- // readonly weekday: CVReal.Type;
682
- // }, MInputError.Type>>
683
- const parser = CVTemplate.toParser(template);
684
-
685
- // Let's define a formatter. Note that there is only one `weekday` property
686
- // Type: (value: {
687
- // readonly weekday: CVReal.Type;
688
- // }) => Either.Either<string, MInputError.Type>
689
- const formatter = CVTemplate.toFormatter(template);
690
-
691
- // Result: { _id: 'Either', _tag: 'Right', right: { weekday: 2 } }
692
- console.log(parser("Today is Tuesday, day number 2 of the week."));
693
-
694
- // Result: {
695
- // _id: 'Either',
696
- // _tag: 'Left',
697
- // left: {
698
- // message: "#weekday is present more than once in template and receives differing values '4' and '2'",
699
- // _tag: '@parischap/effect-lib/InputError/'
700
- // }
701
- // }
702
- console.log(parser("Today is Thursday, day number 2 of the week."));
703
-
704
- // Result: { _id: 'Either', _tag: 'Right', right: 'Today is Saturday, day number 6 of the week.' }
705
- console.log(formatter({ weekday: CVReal.unsafeFromNumber(6) }));
706
-
707
- // Result: {
708
- // _id: 'Either',
709
- // _tag: 'Left',
710
- // left: {
711
- // message: '#weekday: expected one of [1, 2, 3, 4, 5, 6, 7]. Actual: 10',
712
- // _tag: '@parischap/effect-lib/InputError/'
713
- // }
714
- // }
715
- console.log(formatter({ weekday: CVReal.unsafeFromNumber(10) }));
716
- ```
717
-
718
- #### 6. Debugging
719
-
720
- `CVTemplate` objects implement a `.toString()` method that displays a synthetic description of the template followed by the description of each contained `CVTemplatePlaceholder`.
721
-
722
- For instance:
723
-
724
- ```ts
725
- import {
726
- CVNumberBase10Format,
727
- CVTemplate,
728
- CVTemplatePlaceholder,
729
- CVTemplateSeparator,
730
- } from "@parischap/conversions";
731
- import { MRegExpString } from "@parischap/effect-lib";
732
- import { pipe } from "effect";
733
-
734
- // Let's define useful shortcuts
735
- const ph = CVTemplatePlaceholder;
736
- const sep = CVTemplateSeparator;
737
-
738
- // Let's define a template: "#name is a #age-year old #kind."
739
- const template = CVTemplate.make(
740
- // field named 'name' that must be a non-empty string containing no space characters
741
- ph.anythingBut({ name: "name", forbiddenChars: [MRegExpString.space] }),
742
- // Immutable text
743
- sep.make(" is a "),
744
- // Field named 'age' that must represent an unsigned integer
745
- ph.real({
746
- name: "age",
747
- numberBase10Format: pipe(
748
- CVNumberBase10Format.integer,
749
- CVNumberBase10Format.withoutSignDisplay,
750
- ),
751
- }),
752
- // Immutable text
753
- sep.make("-year old "),
754
- // field named 'kind' that must be a non-empty string containing no dot character
755
- ph.anythingBut({ name: "kind", forbiddenChars: ["."] }),
756
- // Immutable text
757
- sep.dot,
758
- );
759
-
760
- // Result:
761
- // #name is a #age-year old #kind.
762
-
763
- // #name: a non-empty string containing non of the following characters: [ \s ].
764
- // #age: unsigned integer.
765
- // #kind: a non-empty string containing non of the following characters: [ . ]
766
- console.log(template);
767
- ```
768
-
769
- ### <a id="DateTimeModule"></a>D) DateTime module
770
-
771
- #### 1. Introduction
772
-
773
- This package implements an immutable `CVDateTime` object: once created, the characteristics of a `CVDateTime` object will never change. However, the provided Setters functions allow you to get a copy of an existing `CVDateTime` object with just one charactreristic modified.
774
-
775
- Although immutable when considered from the outer world, `CVDateTime` objects do keep an internal state that is only used to improve performance (but does not alter results). `CVDateTime` functions can therefore be regarded as pure: they will always yield the same result whatever the state the object is in.
776
-
777
- Unlike the Javascript `Date` objects and the `Effect.DateTime` objects, `CVDateTime` objects handle both
778
- the Gregorian and Iso calendars. So you can easily get/set the iso year and iso week of a
779
- `CVDateTime` object.
780
-
781
- A `CVDateTime` object has a `zoneOffset` which is the difference in hours between the time in the local zone and UTC time (e.g zoneOffset=1 for timezone +1:00). All the data in a `CVDateTime` object is zoneOffset-dependent, except `timestamp`.
782
-
783
- You cannot create a `CVDateTime` object from a string. If this is your need, use the `CVDateTimeFormat` module.
784
-
785
- #### 2. Usage example
786
-
787
- ```ts
788
- import { CVDateTime } from "@parischap/conversions";
789
- import { pipe } from "effect";
790
-
791
- /** You can create a CVDateTime from a timestamp and timeZoneOffset expressed in hours */
792
- // Result: '1970-01-01T05:15:00.000+05:15
793
- console.log(CVDateTime.fromTimestampOrThrow(0, 5.25));
794
-
795
- /**
796
- * You can create a CVDateTime from a timestamp and timeZoneOffset expressed in hours, minutes,
797
- * seconds
798
- */
799
- // Result: '1970-01-01T05:15:00.000+05:15'
800
- console.log(
801
- CVDateTime.fromTimestampOrThrow(0, {
802
- zoneHour: 5,
803
- zoneMinute: 15,
804
- zoneSecond: 0,
805
- }),
806
- );
807
-
808
- /**
809
- * You can create a CVDateTime from a timestamp without specifying a timeZoneOffset. In that case,
810
- * the timeZoneOffset of the machine the code runs on is applied
811
- */
812
- // Result: '1970-01-01T02:00:00.000+02:00' (Was run in Paris during summertime)
813
- console.log(CVDateTime.fromTimestampOrThrow(0));
814
-
815
- /**
816
- * You can create a CVDateTime from DateTime.Parts
817
- *
818
- * See the documentation of function CVDateTime.fromParts to see when and how default values are
819
- * calculated if you don't provide enough information.
820
- *
821
- * Unlike the native Javascript Date object, you cannot pass out-of-range data (e.g month = 13,
822
- * monthDay=31 in April,...). If you pass too much information, all provided parameters must be
823
- * coherent.
824
- *
825
- * Let's see some examples
826
- */
827
-
828
- // Result: { _id: 'Either', _tag: 'Right', right: '2025-01-25T00:00:00.765+00:00' }
829
- console.log(
830
- CVDateTime.fromParts({
831
- year: 2025,
832
- month: 1,
833
- monthDay: 25,
834
- millisecond: 765,
835
- zoneOffset: 0,
836
- }),
837
- );
838
-
839
- // Result: { _id: 'Either', _tag: 'Right', right: '2025-12-30T11:00:00.000-12:00' }
840
- console.log(
841
- CVDateTime.fromParts({
842
- isoYear: 2026,
843
- isoWeek: 1,
844
- weekday: 2,
845
- hour23: 11,
846
- zoneOffset: -12,
847
- }),
848
- );
849
-
850
- // Result: {
851
- // _id: 'Either',
852
- // _tag: 'Left',
853
- // left: {
854
- // message: "Expected 'hour11' to be between 0 (included) and 11 (included). Actual: 12",
855
- // _tag: '@parischap/effect-lib/InputError/'
856
- // }
857
- // }
858
- console.log(
859
- CVDateTime.fromParts({
860
- isoYear: 2026,
861
- isoWeek: 1,
862
- weekday: 2,
863
- hour11: 12,
864
- zoneOffset: -12,
865
- }),
866
- );
867
-
868
- // Result: {
869
- // _id: 'Either',
870
- // _tag: 'Left',
871
- // left: {
872
- // message: "Expected 'monthDay' to be between 1 (included) and 28 (included). Actual: 29",
873
- // _tag: '@parischap/effect-lib/InputError/'
874
- // }
875
- // }
876
- console.log(
877
- CVDateTime.fromParts({ year: 2025, month: 2, monthDay: 29, zoneOffset: 0 }),
878
- );
879
-
880
- // Result: {
881
- // _id: 'Either',
882
- // _tag: 'Left',
883
- // left: {
884
- // message: "Expected 'isoWeek' to be: 9. Actual: 5",
885
- // _tag: '@parischap/effect-lib/InputError/'
886
- // }
887
- // }
888
- console.log(
889
- CVDateTime.fromParts({
890
- year: 2025,
891
- month: 2,
892
- monthDay: 28,
893
- isoWeek: 5,
894
- zoneOffset: 0,
895
- }),
896
- );
897
-
898
- /**
899
- * Once a CVDateTime is created, you can get any CVDateTime.Parts from it ising the provided
900
- * getters. Here are a few examples (you can see the whole list of getters in the API).
901
- */
902
-
903
- const aDate = CVDateTime.fromPartsOrThrow({
904
- year: 1970,
905
- month: 8,
906
- monthDay: 31,
907
- zoneOffset: 0,
908
- });
909
-
910
- // Result: '1970'
911
- console.log(CVDateTime.getYear(aDate));
912
-
913
- // Result: '36'
914
- console.log(CVDateTime.getIsoWeek(aDate));
915
-
916
- // DO NOT DO THIS. It works but is slower because intermediate calculations are not saved
917
- // Result: '1970 36'
918
- console.log(
919
- CVDateTime.getYear(
920
- CVDateTime.fromPartsOrThrow({
921
- year: 1970,
922
- month: 8,
923
- monthDay: 31,
924
- zoneOffset: 0,
925
- }),
926
- ),
927
- CVDateTime.getIsoWeek(
928
- CVDateTime.fromPartsOrThrow({
929
- year: 1970,
930
- month: 8,
931
- monthDay: 31,
932
- zoneOffset: 0,
933
- }),
934
- ),
935
- );
936
-
937
- /**
938
- * Once a CVDateTime is created, you can modify any CVDateTime.Parts with the provided setters. Do
939
- * keep in mind that the initial CVDateTime object is unchanged: you get a copy with the modified
940
- * part. Here are a few examples (you can see the whole list of setters in the API).
941
- */
942
- // Result: { _id: 'Either', _tag: 'Right', right: '1970-03-01T00:00:00.000+00:00' }
943
- console.log(pipe(aDate, CVDateTime.setMonth(3)));
944
-
945
- // result: {
946
- // _id: 'Either',
947
- // _tag: 'Left',
948
- // left: {
949
- // message: 'Month 6 of year 1970 does not have 31 days',
950
- // _tag: '@parischap/effect-lib/InputError/'
951
- // }
952
- // }
953
- console.log(pipe(aDate, CVDateTime.setMonth(6)));
954
-
955
- // Result: { _id: 'Either', _tag: 'Right', right: '1970-08-31T05:45:00.000+05:45' }
956
- console.log(pipe(aDate, CVDateTime.setZoneOffsetKeepTimestamp(5.75)));
957
-
958
- // Result: { _id: 'Either', _tag: 'Right', right: '1970-08-31T00:00:00.000+05:45' }
959
- console.log(pipe(aDate, CVDateTime.setZoneOffsetKeepParts(5.75)));
960
-
961
- /**
962
- * You can also modify the CVDateTime.Parts of an existing CVDateTime object with the provided
963
- * offsetters. Do keep in mind that the initial CVDateTime object is unchanged: you get a copy with
964
- * the modified part. Here are a few examples (you can see the whole list of offsetters in the
965
- * API).
966
- */
967
- // Result: '1970-01-01T00:00:00.000+00:00'
968
- console.log(pipe(aDate, CVDateTime.toFirstYearDay));
969
-
970
- // Result: {
971
- // _id: 'Either',
972
- // _tag: 'Left',
973
- // left: {
974
- // message: 'No February 29th on year 2027 which is not a leap year',
975
- // _tag: '@parischap/effect-lib/InputError/'
976
- // }
977
- // }
978
- console.log(
979
- pipe(
980
- CVDateTime.fromPartsOrThrow({
981
- year: 2024,
982
- month: 2,
983
- monthDay: 29,
984
- zoneOffset: 0,
985
- }),
986
- CVDateTime.offsetYears(3, false),
987
- ),
988
- );
989
-
990
- // Result: { _id: 'Either', _tag: 'Right', right: '2028-02-29T00:00:00.000+00:00' }
991
- console.log(
992
- pipe(
993
- CVDateTime.fromPartsOrThrow({
994
- year: 2024,
995
- month: 2,
996
- monthDay: 29,
997
- zoneOffset: 0,
998
- }),
999
- CVDateTime.offsetYears(4, false),
1000
- ),
1001
- );
1002
-
1003
- /** And finally you can use one of the few provided predicates whose list you will find in the API */
1004
-
1005
- // Result: true
1006
- console.log(CVDateTime.isLastMonthDay(aDate));
1007
-
1008
- // Result: false
1009
- console.log(CVDateTime.isFirstMonthDay(aDate));
1010
- ```
1011
-
1012
- ### <a id="DateTimeParserFormatter"></a>E) DateTime parser/formatter
1013
-
1014
- #### 1. Usage example
1015
-
1016
- ```ts
1017
- import {
1018
- CVDateTime,
1019
- CVDateTimeFormat,
1020
- CVDateTimeFormatContext,
1021
- CVSchema,
1022
- } from "@parischap/conversions";
1023
- import { DateTime, Either, flow, Schema } from "effect";
1024
-
1025
- // Let's define useful shortcuts
1026
- const placeholder = CVDateTimeFormat.TemplatePart.Placeholder.make;
1027
- const sep = CVDateTimeFormat.TemplatePart.Separator;
1028
-
1029
- // Let's define a context
1030
- const frenchContext = CVDateTimeFormatContext.fromLocaleOrThrow("fr-FR");
1031
-
1032
- // Let's define a DateTimeFormat: iiii d MMMM yyyy
1033
- const frenchFormat = CVDateTimeFormat.make({
1034
- context: frenchContext,
1035
- templateparts: [
1036
- placeholder("iiii"),
1037
- sep.space,
1038
- placeholder("d"),
1039
- sep.space,
1040
- placeholder("MMMM"),
1041
- sep.space,
1042
- placeholder("yyyy"),
1043
- ],
1044
- });
1045
-
1046
- // Let's define a parser
1047
- // Type: (dateString: string) => Either.Either<CVDateTime.Type, MInputError.Type>
1048
- const parser = CVDateTimeFormat.toParser(frenchFormat);
1049
-
1050
- // Let's define a formatter
1051
- // Type: (date: CVDateTime.Type) => Either.Either<string, MInputError.Type>
1052
- const formatter = CVDateTimeFormat.toFormatter(frenchFormat);
1053
-
1054
- // Let's define a parser to Effect.DateTime for Effect users
1055
- // Type: (dateString: string) => Either.Either<DateTime.Zoned, MInputError.Type>
1056
- const effectParser = flow(parser, Either.map(CVDateTime.toEffectDateTime));
1057
-
1058
- // Let's define a formatter from Effect.DateTime for Effect users
1059
- // Type: (date: DateTime.Zoned) => Either.Either<string, MInputError.Type>
1060
- const effectFormatter = flow(CVDateTime.fromEffectDateTime, formatter);
1061
-
1062
- // Let's define a parser that returns a date or throws for non Effect users
1063
- // Type: (dateString: string) => Date
1064
- const jsParser = flow(
1065
- CVDateTimeFormat.toThrowingParser(frenchFormat),
1066
- CVDateTime.toDate,
1067
- );
1068
-
1069
- // Let's define a formatter that takes a date and throws for non Effect users
1070
- // Type: (date: Date) => string
1071
- const jsFormatter = flow(
1072
- CVDateTime.fromDate,
1073
- CVDateTimeFormat.toThrowingFormatter(frenchFormat),
1074
- );
1075
-
1076
- // Result: {
1077
- // _id: 'Either',
1078
- // _tag: 'Left',
1079
- // left: {
1080
- // message: "Expected remaining text for #weekday to start with one of [lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche]. Actual: '20201210'",
1081
- // _tag: '@parischap/effect-lib/InputError/'
1082
- // }
1083
- // }
1084
- console.log(parser("20201210"));
1085
-
1086
- // Result: {
1087
- // _id: 'Either',
1088
- // _tag: 'Left',
1089
- // left: {
1090
- // message: "Expected 'weekday' to be: 4. Actual: 1",
1091
- // _tag: '@parischap/effect-lib/InputError/'
1092
- // }
1093
- // }
1094
- console.log(parser("lundi 4 septembre 2025"));
1095
-
1096
- // Result: { _id: 'Either', _tag: 'Right', right: '2025-09-04T00:00:00.000+02:00' }
1097
- console.log(parser("jeudi 4 septembre 2025"));
1098
-
1099
- // Result: { _id: 'Either', _tag: 'Right', right: '2025-09-03T22:00:00.000Z' }
1100
- console.log(effectParser("jeudi 4 septembre 2025"));
1101
-
1102
- // Result: '2025-09-03T22:00:00.000Z'
1103
- console.log(jsParser("jeudi 4 septembre 2025"));
1104
-
1105
- // Result: { _id: 'Either', _tag: 'Right', right: 'jeudi 1 janvier 1970' }
1106
- console.log(formatter(CVDateTime.fromTimestampOrThrow(0, 0)));
1107
-
1108
- // Result: { _id: 'Either', _tag: 'Right', right: 'jeudi 1 janvier 1970' }
1109
- console.log(effectFormatter(DateTime.unsafeMakeZoned(0, { timeZone: 0 })));
1110
-
1111
- // Result: 'jeudi 1 janvier 1970'
1112
- console.log(jsFormatter(new Date(0)));
1113
-
1114
- // Result: {
1115
- // _id: 'Either',
1116
- // _tag: 'Left',
1117
- // left: {
1118
- // message: 'Expected length of #year to be: 4. Actual: 5',
1119
- // _tag: '@parischap/effect-lib/InputError/'
1120
- // }
1121
- console.log(formatter(CVDateTime.fromPartsOrThrow({ year: 10024 })));
1122
-
1123
- // Using Schema
1124
- const schema = CVSchema.DateTime(frenchFormat);
1125
-
1126
- // For Effect users
1127
- const effectSchema = CVSchema.DateTimeZoned(frenchFormat);
1128
-
1129
- // For non Effect users
1130
- const jsSchema = CVSchema.Date(frenchFormat);
1131
-
1132
- // Type: (value: string ) => Either.Either<CVDateTime.Type,ParseError>
1133
- const decoder = Schema.decodeEither(schema);
1134
-
1135
- // Type: (value: CVDateTime.Type ) => Either.Either<string,ParseError>
1136
- const encoder = Schema.encodeEither(schema);
1137
-
1138
- // Type: (value: string ) => Either.Either<DateTime.Zoned,ParseError>
1139
- const effectDecoder = Schema.decodeEither(effectSchema);
1140
-
1141
- // Type: (value: CVDateTime.Zoned ) => Either.Either<string,ParseError>
1142
- const effectEncoder = Schema.encodeEither(effectSchema);
1143
-
1144
- // Type: (value: string ) => Either.Either<Date,ParseError>
1145
- const jsDecoder = Schema.decodeEither(jsSchema);
1146
-
1147
- // Type: (value: Date ) => Either.Either<string,ParseError>
1148
- const jsEncoder = Schema.encodeEither(jsSchema);
1149
-
1150
- // Result: { _id: 'Either', _tag: 'Right', right: '2025-09-04T00:00:00.000+02:00' }
1151
- console.log(decoder("jeudi 4 septembre 2025"));
1152
-
1153
- // Error: Expected 'weekday' to be: 4. Actual: 1
1154
- console.log(decoder("lundi 4 septembre 2025"));
1155
-
1156
- // Result: { _id: 'Either', _tag: 'Right', right: 'jeudi 1 janvier 1970' }
1157
- console.log(encoder(CVDateTime.fromTimestampOrThrow(0, 0)));
1158
-
1159
- // Result: { _id: 'Either', _tag: 'Right', right: '2025-09-03T22:00:00.000Z' }
1160
- console.log(effectDecoder("jeudi 4 septembre 2025"));
1161
-
1162
- // Result: { _id: 'Either', _tag: 'Right', right: 'jeudi 1 janvier 1970' }
1163
- console.log(effectEncoder(DateTime.unsafeMakeZoned(0, { timeZone: 0 })));
1164
-
1165
- // Result: { _id: 'Either', _tag: 'Right', right: 2025-09-03T22:00:00.000Z }
1166
- console.log(jsDecoder("jeudi 4 septembre 2025"));
1167
-
1168
- // Result: { _id: 'Either', _tag: 'Right', right: 'jeudi 1 janvier 1970' }
1169
- console.log(jsEncoder(new Date(0)));
1170
- ```
1171
-
1172
- #### 2. Available tokens
1173
-
1174
- Many of the available [unicode tokens](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table) can be used to define `CVDateTimeFormat`'s. Here is a list of all currently available tokens:
1175
-
1176
- ```ts
1177
- export type Token =
1178
- /* Gregorian year (ex: 2005) */
1179
- | "y"
1180
- /* Gregorian year on 2 digits left-padded with 0's corresponding to years 2000-2099 (ex: 05 for 2005) */
1181
- | "yy"
1182
- /* Gregorian year on 4 digits left-padded with 0's (ex: 2005, 0965) */
1183
- | "yyyy"
1184
- /* Iso year (ex: 2005) */
1185
- | "R"
1186
- /* Iso year on 2 digits left-padded with 0's corresponding to years 2000-2099 (ex: 05 for 2005) */
1187
- | "RR"
1188
- /* Iso year on 4 digits left-padded with 0's (ex: 2005, 0965)*/
1189
- | "RRRR"
1190
- /* Month (ex: 6) */
1191
- | "M"
1192
- /* Month on 2 digits left-padded with 0's (ex: 06) */
1193
- | "MM"
1194
- /* Short month name (ex: Jun) */
1195
- | "MMM"
1196
- /* Long month name (ex: June) */
1197
- | "MMMM"
1198
- /* IsoWeek (ex: 6) */
1199
- | "I"
1200
- /* IsoWeek (ex: 06) */
1201
- | "II"
1202
- /* Day of month (ex: 5) */
1203
- | "d"
1204
- /* Day of month on 2 digits left-padded with 0's (ex: 05) */
1205
- | "dd"
1206
- /* Day of year (ex: 97) */
1207
- | "D"
1208
- /* Day of year on 3 digits left-padded with 0's (ex: 097) */
1209
- | "DDD"
1210
- /* Weekday (ex: 1 for monday, 7 for sunday) */
1211
- | "i"
1212
- /* Short weekday name (ex: Mon) */
1213
- | "iii"
1214
- /* Long weekday name (ex: Monday) */
1215
- | "iiii"
1216
- /* Meridiem (ex: 'AM' for 0, 'PM' for 12) */
1217
- | "a"
1218
- /* Hour in the range 0..23 (ex:5, 14) */
1219
- | "H"
1220
- /* Hour on 2 digits in the range 0..23 left-padded with 0's (ex:05, 14) */
1221
- | "HH"
1222
- /* Hour in the range 0..11 (ex:5, 2) */
1223
- | "K"
1224
- /* Hour on 2 digits in the range 0..11 left-padded with 0's (ex:05, 02) */
1225
- | "KK"
1226
- /* Minute (ex: 5) */
1227
- | "m"
1228
- /* Minute on 2 digits left-padded with 0's (ex: 05) */
1229
- | "mm"
1230
- /* Second (ex: 5) */
1231
- | "s"
1232
- /* Second on 2 digits left-padded with 0's (ex: 05) */
1233
- | "ss"
1234
- /* Millisecond (ex: 5) */
1235
- | "S"
1236
- /* Millisecond on 3 digits left-padded with 0's (ex: 005) */
1237
- | "SSS"
1238
- /* Hour part of the timezone offset (ex: 5) */
1239
- | "zH"
1240
- /* Hour part of the timezone offset on 2 digits left-padded with 0's (ex: 05) */
1241
- | "zHzH"
1242
- /* Minute part of the timezone offset (ex: 5) */
1243
- | "zm"
1244
- /* Minute part of the timezone offset on 2 digits left-padded with 0's (ex: 05) */
1245
- | "zmzm"
1246
- /* Second part of the timezone offset (ex: 5) */
1247
- | "zs"
1248
- /* Second part of the timezone offset on 2 digits left-padded with 0's (ex: 05) */
1249
- | "zszs";
1250
- ```
1251
-
1252
- #### 3. CVDateTimeFormatContext
1253
-
1254
- Some of the available tokens are language specific. For instance the `MMMM` token is expected to display `december` in English and `décembre` in French. For this reason, you need to build a `CVDateTimeFormatContext` before building a `CVDateTimeFormat`. You can build a `CVDateTimeFormatContext` in one of the three following ways:
1255
-
1256
- - you can use the provided `CVDateTimeFormatContext.enGB` instance (for Great Britain English language)
1257
- - you can build a `CVDateTimeFormatContext` from the name of a locale, e.g. `const frenchContext = CVDateTimeFormatContext.fromLocaleOrThrow("fr-FR")`
1258
- - if you have very specific needs or your locale is not available, you can build a `CVDateTimeFormatContext` by providing directly your translations to the `CVDateTimeFormatContext.fromNames` constructor.
1259
-
1260
- #### 4. Debugging
1261
-
1262
- `CVDateTimeFormat` objects implement a `.toString()` method which displays a synthetic description of the template followed by the description of each CVPlaceholder. For instance:
1263
-
1264
- ```ts
1265
- import {
1266
- CVDateTimeFormat,
1267
- CVDateTimeFormatContext,
1268
- } from "@parischap/conversions";
1269
-
1270
- // Let's define useful shortcuts
1271
- const placeholder = CVDateTimeFormat.TemplatePart.Placeholder.make;
1272
- const sep = CVDateTimeFormat.TemplatePart.Separator;
1273
-
1274
- // Let's define a DateTimeFormat: iiii d MMMM yyyy
1275
- const frenchFormat = CVDateTimeFormat.make({
1276
- context: CVDateTimeFormatContext.enGB,
1277
- templateparts: [
1278
- placeholder("iiii"),
1279
- sep.space,
1280
- placeholder("d"),
1281
- sep.space,
1282
- placeholder("MMMM"),
1283
- sep.space,
1284
- placeholder("yyyy"),
1285
- ],
1286
- });
1287
-
1288
- // Result: "'iiii d MMMM yyyy' in 'en-GB' context"
1289
- console.log(frenchFormat);
1290
- ```
1291
-
1292
- ### <a id="Branding"></a>F) Branding
1293
-
1294
- #### 1. Introduction
1295
-
1296
- In this package you will find the following [`Brand`'s](https://effect.website/docs/code-style/branded-types/):
1297
-
1298
- - `CVEmail`: represents a valid email string
1299
- - `CVSemVer`: represents a valid semantic versioning string
1300
- - `CVReal`: represents a valid floating-point number (+Infinity, Infinity, -Infinity, NaN not allowed). Can be used to represent a temperature, a height from sea-level,...
1301
- - `CVPositiveReal`: same as `CVReal` but the number must be positive. Can be used to represent a price, a speed,...
1302
- - `CVInteger`: same as `CVReal` but the number must be an integer. Can be used to represent a floor in a lift, a signed quantity...
1303
- - `CVPositiveInteger`: same as `CVInteger` but the number must be positive. Can be used to represent an age, a quantity,...
1304
-
1305
- You will also find all the functions to convert from one brand to another. Do not hesitate to take a look at the [API](https://parischap.github.io/effect-libs/docs/conversions) to learn more about what this module offers in terms of branding.
package/package.json CHANGED
@@ -13,8 +13,17 @@
13
13
  ],
14
14
  "keywords": [
15
15
  "number",
16
- "text",
16
+ "date",
17
+ "format",
18
+ "parse",
19
+ "formatting",
20
+ "parsing",
21
+ "rounding",
22
+ "templating",
17
23
  "conversion",
24
+ "sscanf",
25
+ "sprintf",
26
+ "text",
18
27
  "n2t",
19
28
  "num2text",
20
29
  "convert",
@@ -24,7 +33,7 @@
24
33
  "typescript",
25
34
  "functional-programming"
26
35
  ],
27
- "description": "A functional library to convert number and dates to string and vice-versa",
36
+ "description": "A functional library to replace partially the native Intl API",
28
37
  "module": "./esm/index.js",
29
38
  "exports": {
30
39
  ".": {
@@ -43,7 +52,7 @@
43
52
  "directory": "packages/conversions"
44
53
  },
45
54
  "homepage": "https://github.com/parischap/effect-libs/tree/master/packages/conversions",
46
- "version": "0.3.0",
55
+ "version": "0.4.0",
47
56
  "main": "./cjs/index.js",
48
57
  "types": "./dts/index.d.ts"
49
58
  }