UNPKG

@parischap/conversions

Version:

A functional library to replace partially the native Intl API

371 lines 14.2 kB
/** * This module implements a `CVTemplatePlaceholder` type which is one of the constituents of * `CVTemplate`'s (see Template.ts and TemplatePart.ts) * * Each `CVTemplatePlaceholder` defines a parser and a formatter: * * - 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`. * - 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, returns a `Right` of that string. Otherwise, it returns a * `Left` */ import * as MInputError from '@parischap/effect-lib/MInputError'; import * as MInspectable from '@parischap/effect-lib/MInspectable'; import * as MPipeable from '@parischap/effect-lib/MPipeable'; import * as MRegExp from '@parischap/effect-lib/MRegExp'; import * as MRegExpString from '@parischap/effect-lib/MRegExpString'; import * as MString from '@parischap/effect-lib/MString'; import * as MStruct from '@parischap/effect-lib/MStruct'; import * as MTuple from '@parischap/effect-lib/MTuple'; import * as MTypes from '@parischap/effect-lib/MTypes'; import * as Array from 'effect/Array'; import * as Either from 'effect/Either'; import {flow} from 'effect/Function'; import * as Function from 'effect/Function'; import * as HashMap from 'effect/HashMap'; import {pipe} from 'effect/Function'; import * as Predicate from 'effect/Predicate'; import * as Schema from 'effect/Schema'; import * as String from 'effect/String'; import * as Struct from 'effect/Struct'; import * as Tuple from 'effect/Tuple'; import * as CVNumberBase10Format from './NumberBase10Format.js'; import * as CVReal from './Real.js'; /** * Module tag * * @category Module markers */ export const moduleTag = '@parischap/conversions/TemplatePlaceholder/'; const _TypeId = /*#__PURE__*/Symbol.for(moduleTag); /** * Type guard * * @category Guards */ export const has = u => Predicate.hasProperty(u, _TypeId); /** Proto */ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const proto = { [_TypeId]: { _N: MTypes.covariantValue, _T: MTypes.invariantValue }, [MInspectable.IdSymbol]() { return getLabelledDescription(this); }, ... /*#__PURE__*/MInspectable.BaseProto(moduleTag), ...MPipeable.BaseProto }; const _make = params => MTypes.objectFromDataAndProto(proto, params); /** * Constructor * * @category Constructors */ export const make = /*#__PURE__*/flow(/*#__PURE__*/MStruct.enrichWith({ label: ({ name }) => MString.prepend('#')(name) }), _make); /** * Returns the `name` property of `self` * * @category Destructors */ export const name = /*#__PURE__*/Struct.get('name'); /** * Returns the `label` property of `self` * * @category Destructors */ export const label = /*#__PURE__*/Struct.get('label'); /** * Returns the `description` property of `self` * * @category Destructors */ export const description = /*#__PURE__*/Struct.get('description'); /** * Returns the `parser` property of `self` * * @category Destructors */ export const parser = /*#__PURE__*/Struct.get('parser'); /** * Returns the `formatter` property of `self` * * @category Destructors */ export const formatter = /*#__PURE__*/Struct.get('formatter'); /** * Returns the `tSchemaInstance` property of `self` * * @category Destructors */ export const tSchemaInstance = /*#__PURE__*/Struct.get('tSchemaInstance'); /** * Returns a description of `self`, e.g. "#dd: 2-character string left-padded with '0' to unsigned * integer." * * @category Destructors */ export const getLabelledDescription = self => `${self.label}: ${self.description}`; /** * Returns a copy of `self` where a postParser function is executed after the parser of `self` and a * preFormatter function is executed before the formatter of `self` * * @category Destructors */ export const modify = ({ descriptorMapper, postParser, preFormatter, t1SchemaInstance }) => self => make({ name: self.name, description: descriptorMapper(self.description), parser: function (text) { return Either.flatMap(self.parser.call(this, text), flow(Tuple.mapBoth({ onFirst: t => postParser.call(this, t), onSecond: Either.right }), Either.all)); }, formatter: function (t1) { return pipe(preFormatter.call(this, t1), Either.flatMap(t => self.formatter.call(this, t))); }, tSchemaInstance: t1SchemaInstance === undefined ? self.tSchemaInstance : t1SchemaInstance }); /** * Builds a `CVTemplatePlaceholder` instance that parses/formats exactly `length` characters from a * string. `length` must be a strictly positive integer. * * @category Constructors */ export const fixedLength = ({ name, length }) => { return make({ name, description: `${length}-character string`, parser: function (text) { return pipe(text, MString.splitAt(length), Tuple.mapBoth({ onFirst: MInputError.assertLength({ expected: length, name: this.label }), onSecond: Either.right }), Either.all); }, formatter: function (value) { return MInputError.assertLength({ expected: length, name: this.label })(value); }, tSchemaInstance: Schema.String }); }; /** * Same as `fixedLength` but the consumed text is trimmed off of a `fillChar` at `fillPosition` and * the written text is padded with a `fillChar` at `fillPosition`. `fillChar` should be a * one-character string. `length` must be a strictly positive integer. See the meaning of * `disallowEmptyString` in `MString.trim` * * @category Constructors */ export const paddedFixedLength = params => { const trimmer = flow(MString.trim(params), Either.right); const padder = flow(MString.pad(params), Either.right); return pipe(fixedLength(params), modify({ descriptorMapper: MString.append(` ${MString.FillPosition.toId(params.fillPosition)}-padded with '${params.fillChar}'`), postParser: trimmer, preFormatter: padder })); }; /** * 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. If the number to parse/format is less than `length` characters, * `fillChar` is trimmed/padded between the sign and the number so that the length condition is * respected. `fillChar` must be a one-character string (but no error is triggered if you do not * respect that condition) * * @category Constructors */ export const fixedLengthToReal = params => { const { numberBase10Format, fillChar } = params; const numberParser = function (input) { return pipe(input, CVNumberBase10Format.toRealParser(numberBase10Format, fillChar), Either.fromOption(() => new MInputError.Type({ message: `${this.label}: value '${input}' cannot be converted to a(n) ${CVNumberBase10Format.toDescription(numberBase10Format)}` }))); }; const numberFormatter = flow(CVNumberBase10Format.toNumberFormatter(numberBase10Format, pipe(fillChar, String.repeat(params.length))), Either.right); return pipe(fixedLength(params), modify({ descriptorMapper: MString.append(` left-padded with '${fillChar}' to ${CVNumberBase10Format.toDescription(numberBase10Format)}`), postParser: numberParser, preFormatter: numberFormatter, t1SchemaInstance: CVReal.SchemaFromSelf })); }; /** * Builds a `CVTemplatePlaceholder` whose parser reads from the text all the characters that it can * interpret as a number in the provided `numberBase10Format` and converts the consumed text into a * `CVReal`. The formatter takes a `CVReal` and converts it into a string according to the provided * `numberBase10Format`. * * @category Constructors */ export const real = ({ name, numberBase10Format }) => { const numberParser = CVNumberBase10Format.toRealExtractor(numberBase10Format); const numberFormatter = CVNumberBase10Format.toNumberFormatter(numberBase10Format); const flippedTakeRightBut = Function.flip(MString.takeRightBut); return make({ name, description: `${CVNumberBase10Format.toDescription(numberBase10Format)}`, parser: function (text) { return pipe(text, numberParser, Either.fromOption(() => new MInputError.Type({ message: `${this.label} contains '${text}' from the start of which a(n) ${CVNumberBase10Format.toDescription(numberBase10Format)} could not be extracted` })), Either.map(Tuple.mapSecond(flow(String.length, flippedTakeRightBut(text))))); }, formatter: flow(numberFormatter, Either.right), tSchemaInstance: CVReal.SchemaFromSelf }); }; /** * Builds a `CVTemplatePlaceholder` instance that works as a map: * * The parser expects one of the keys of `keyValuePairs` and will return the associated value. The * formatter expects one of the values of `keyValuePairs` and will return the associated key. * * `keyValuePairs` should define a bijection (each key and each value must be present only once). It * is best if the type of the values defines a toString method. Value equality is checked with The * Effect Equal.equals function. * * `schemaInstance` is a `Schema` instance that transforms a value of type T into a value of type T. * It is an optional parameter. You need only provide it if you intend to use a `CVTemplate` built * from this `CVTemplatePlaceholder` within the `Effect.Schema` module. In that case, you can build * such a `Schema` with the `Schema.declare` function (if you don't provide it, the `Schema` will * return an error) * * @category Constructors */ export const mappedLiterals = ({ name, keyValuePairs, schemaInstance = Schema.declare(_input => false) }) => { const keys = pipe(keyValuePairs, Array.map(Tuple.getFirst), Array.join(', '), MString.prepend('['), MString.append(']')); const values = pipe(keyValuePairs, Array.map(flow(Tuple.getSecond, MString.fromUnknown)), Array.join(', '), MString.prepend('['), MString.append(']')); const valueNameMap = pipe(keyValuePairs, Array.map(Tuple.swap), HashMap.fromIterable); const isTheStartOf = Function.flip(String.startsWith); const flippedTakeRightBut = Function.flip(MString.takeRightBut); return make({ name, description: `from ${keys} to ${values}`, parser: function (text) { return pipe(keyValuePairs, Array.findFirst(flow(Tuple.getFirst, isTheStartOf(text))), Either.fromOption(() => new MInputError.Type({ message: `Expected remaining text for ${this.label} to start with one of ${keys}. Actual: '${text}'` })), Either.map(MTuple.makeBothBy({ toFirst: Tuple.getSecond, toSecond: flow(Tuple.getFirst, String.length, flippedTakeRightBut(text)) }))); }, formatter: function (value) { return pipe(valueNameMap, HashMap.get(value), Either.fromOption(() => new MInputError.Type({ message: `${this.label}: expected one of ${values}. Actual: ${MString.fromUnknown(value)}` }))); }, tSchemaInstance: schemaInstance }); }; /** * Same as `mappedLiterals` but `T` is assumed to be `CVReal` which should be the most usual use * case * * @category Constructors */ export const realMappedLiterals = params => mappedLiterals({ ...params, schemaInstance: CVReal.SchemaFromSelf }); /** * Builds a `CVTemplatePlaceholder` whose parser 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. `regExp` must start with the ^ character. * Otherwise, the parser and formatter will not work properly. * * @category Constructors */ export const fulfilling = ({ name, regExp, regExpDescriptor }) => { const flippedTakeRightBut = Function.flip(MString.takeRightBut); const match = label => MInputError.match({ regExp, regExpDescriptor, name: label }); return make({ name, description: `${regExpDescriptor}`, parser: function (text) { return pipe(text, match(this.label), Either.map(MTuple.makeBothBy({ toFirst: Function.identity, toSecond: flow(String.length, flippedTakeRightBut(text)) }))); }, formatter: function (text) { return pipe(text, match(this.label), Either.filterOrLeft(MString.hasLength(text.length), () => new MInputError.Type({ message: `${this.label}: expected ${regExpDescriptor}. Actual: '${text}'` }))); }, tSchemaInstance: Schema.String }); }; /** * This `CVTemplatePlaceholder` instance is a special case of the `fulfilling` * `CVTemplatePlaceholder` instance. The parser of this Placeholder reads from the text until it * meets one of the `forbiddenChars` passed as parameter (the result must be a non-empty string). * The formatter only accepts a non-empty string that does not contain any of the forbidden chars * and write it to the text. `forbiddenChars` should be an array of 1-character strings (will not * throw otherwise but strange behaviors can be expected) * * @category Constructors */ export const anythingBut = ({ name, forbiddenChars }) => { const forbiddenCharsAsString = pipe(forbiddenChars, Array.join("', '"), MString.prepend("[ '"), MString.append("' ]")); return fulfilling({ name, regExp: pipe(forbiddenChars, MRegExpString.notInRange, MRegExpString.oneOrMore, MRegExpString.atStart, MRegExp.fromRegExpString()), regExpDescriptor: `a non-empty string containing non of the following characters: ${forbiddenCharsAsString}` }); }; /** * This `CVTemplatePlaceholder` instance is another special case of the `fulfilling` * `CVTemplatePlaceholder` instance. The parser of this `CVTemplatePlaceholder` 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`. * * @category Constructors */ export const toEnd = name => fulfilling({ name, regExp: /^.*/, regExpDescriptor: 'a string' }); //# sourceMappingURL=TemplatePlaceholder.js.map