Skip to content

Instantly share code, notes, and snippets.

@renoirb
Last active February 7, 2023 19:58
Show Gist options
  • Save renoirb/16f391e0cbd4e4e04f368c06b396e650 to your computer and use it in GitHub Desktop.
Save renoirb/16f391e0cbd4e4e04f368c06b396e650 to your computer and use it in GitHub Desktop.
Some useful snippets

Some other useful snippets

Prettify

Found on Twitter

Imagine you've got some type that's got a bunch of intersections. When you hover over it, it gives you a bunch of gross intersections instead of displaying the resolved type

export type Flatten<T> = {
  [K in keyof T]: T[K];
} & {}

isObject

export const isObject = <T = Record<string, unknown>>(value: unknown): value is T => {
  // https://github.com/lodash/lodash/blob/4.17.21-es/isObject.js
  // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/ca8d73db/types/lodash.isobject/index.d.ts
  const type = typeof value
  return value != null && (type == 'object' || type == 'function')
}

DOM user-defined Type Guards

export const isHtmlElement = (maybe: unknown): maybe is HTMLElement => {
  if (maybe && typeof maybe === 'function' && 'prototype' in maybe) {
    return 'title' in maybe || 'lang' in maybe || 'offsetParent' in maybe
  }
  return false
}

export const isElement = (maybe: unknown): maybe is Element => {
  if (isHtmlElement(maybe)) {
    return 'localName' in maybe || 'shadowRoot' in maybe
  }
  return false
}

export const isNode = (maybe: unknown): maybe is Node => {
  if (isElement(maybe)) {
    return 'ATTRIBUTE_NODE' in maybe || 'nodeName' in maybe || 'parentElement' in maybe || 'parentNode' in maybe
  }
  return false
}

Walking Record, anonymize one field

export const toAnonymizedCredentialMapString: (input: Record<string, unknown>) => string = input => {
  assert.equal(isObject(input), true, `Is not an HashMap object`)
  const copy: Record<string, string> = JSON.parse(JSON.stringify(input || {}))
  for (const [field, value] of Object.entries(copy)) {
    if (field === 'password' && value !== '') {
      copy[field] = '***'
    }
  }
 
  return JSON.stringify(copy)
}

Is Object

export const isObject = <T = Record<string, unknown>>(value: unknown): value is T => {
  // https://github.com/lodash/lodash/blob/4.17.21-es/isObject.js
  // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/ca8d73db/types/lodash.isobject/index.d.ts
  const type = typeof value
  return value != null && (type == 'object' || type == 'function')
}

Record creation

export const TestUser = Object.freeze({
  /**
   * Account of someone who did upload their avatar photo
   */
  someUserAccountWithParticularRole: 'someUserAccountWithParticularRole',
} as const)
 
/**
 * Based on TestUser enum above, what are the acceptable keys.
 *
 * See:
 * - https://gist.github.com/renoirb/e8f48e4b54f16f9fb8f0bf38b6bc9c8d#using-read-only-array-of-strings-as-source-for-type
 * - https://gist.github.com/renoirb/d30e7edbe32d9c6b9c2006b13db7a016
 */
export const TEST_USERS_KEYS = Object.freeze(
  Object.entries(TestUser)
    .filter(i => 1 in i && typeof i[1] === 'string')
    .map(i => i[0])
    .sort(),
) as TestUserKeys[]

export type ITestUser = typeof TEST_USERS_KEYS[number]


export type ICredential {
  username: string
  password: string
}

export type ITestUserCredientialMap = Record<ITestUser, ICredential>

export const readCredentialMapFromProcessEnv = (
  input: ITestUserCredientialMap,
  env: NodeJS.ProcessEnv
): ITestUserCredientialMap => {
  const username = Reflect.has(env, input.username) ? Reflect.get(env, input.username) : null
  const password = Reflect.has(env, input.password) ? Reflect.get(env, input.password) : null
  const missing = Object.entries<Record<string, string | void>>({ username, password })
    .map(v => (v[1] === null ? `${v[0]}: ${Reflect.get(input, v[0])}` : undefined))
    .filter(Boolean)
  if (missing.length > 0) {
    const message = `We could not find process.env.<key> for: ${missing.join(', ')}`
    throw new Error(message)
  }
  const out: ITestUserCredientialMap = {
    username,
    password,
  }
  // assertIsTestUserCredientialMap(out)
  return out
}

export const TEST_USERS_CREDENTIAL_MAP: ITestUserCredientialMap = {
  someUserAccountWithParticularRole: {
    username: 'jdoe',
    password: 'password123!',
  },
}

NestedError

/**
 * Subclass of standard `Error` that eagerly collects the callstack of the error
 * that caused it. This way you can investigate the core problem that happened
 * by looking at the callstack from up to bottom (from higher level errors to
 * lower level).
 *
 * Bookmark:
 * - https://github.com/Veetaha/ts-nested-error/blob/2bb11d66/src/nested-error.ts#L7-L86
 */
export class NestedError extends Error {
  /**
   * Combined callstack of this error and the errors that it wraps.
   * If the JavaScript runtime doesn't support `Error::stack` property
   * this will contain only the concatenated messages.
   */
  readonly stack: string

  /**
   * The list of lower-level errors wrapped by this error.
   */
  readonly innerErrors: Error[]

  private static readonly getErrorReport =
    typeof new Error().stack === 'string' ? (err: Error) => err?.stack : (err: Error) => `${err.name}: ${err.message}`

  /**
   * Returns the function that accepts any value that was thrown as the first argument and
   * throws it wrapped into `NestedError` or class derived from `NestedError` (provided
   * this method was called directly in the context of that dervied class constructor)
   * with the given `message`.
   * Returned function will pass accepted `Error` object directly to `NestedError`
   * as `innerErrors` by invoking `toError(err)` on it.
   *
   * You'll most likely want to use this method with promises:
   *
   * ```ts
   * userService.getPage().then(
   *     data => console.log(`Hooray! data: ${data}`),
   *     NestedError.rethrow('failed to fetch users page')
   * );
   * ```
   *
   * @param message Message to attach `NestedError` created by the returned function.
   */
  static rethrow(message: string) {
    return (...errs: unknown[]) => {
      throw new this(message, ...errs)
    }
  }

  /**
   * Allocates an instance of `NestedError` with the given error `message` and
   * optional `innerError` (which will be automatically coerced using `toError()`).
   *
   * @param message     Laconic error message to attach to the created `NestedError`.
   * @param innerErrors Optional errors that will be wrapped by this higher level
   *                    error. This value will be automatically coerced using `toError()`.
   */
  constructor(message?: string, ...innerErrors: unknown[]) {
    super(message)
    const thisErrorReport = NestedError.getErrorReport(this)
    if (innerErrors.length === 1) {
      const innerError = toError(innerErrors[0])
      this.innerErrors = [innerError]
      const errReport = NestedError.getErrorReport(innerError)
      this.stack = `${thisErrorReport}\n\n======= INNER ERROR =======\n\n${errReport}`
      return
    }
    this.innerErrors = innerErrors.map(err => toError(err))
    const innerErrorReports = this.innerErrors
      .map((error, idx) => {
        const errReport = NestedError.getErrorReport(error)
        return `======= INNER ERROR (${idx + 1} of ${innerErrors.length}) =======\n\n${errReport}`
      })
      .join('\n\n')
    this.stack = `${thisErrorReport}\n\n${innerErrorReports}`
  }
}

/**
 * Returns `err` itself if `err instanceof Error === true`, otherwise attemts to
 * stringify it and wrap into `Error` object to be returned.
 *
 * **This function is guaranteed never to throw.**
 *
 * @param err Possbile `instanceof Error` to return or value of any type that will
 *            be wrapped into a fully-fledged `Error` object.
 *
 * Bookmark:
 * - https://github.com/Veetaha/ts-nested-error/blob/2bb11d66/src/nested-error.ts#L97-L120
 */
export function toError(err: unknown): Error

export function toError(err: unknown) {
  try {
    return err instanceof Error ? err : new Error(`Value that is not an instance of Error was thrown: ${err}`)
  } catch {
    return new Error(
      'Failed to stringify non-instance of Error that was thrown.' +
        'This is possibly due to the fact that toString() method of the value' +
        "doesn't return a primitive value.",
    )
  }
}

/**
 * Not Implemented Exception
 *
 * To signify that we should implement this method or function
 */
export class NotImplementedException extends NestedError {
  constructor(message = '', innerException?: Error) {
    const fallbackMessage = 'not implemented'
    const msg = message !== '' && /\s/.test(message) === false ? `${message} is ${fallbackMessage}` : fallbackMessage
    super(msg, innerException)
  }
}

ReadOnlySet

export const createReadOnlySet = <T>(items: readonly string[] = []): ReadonlySet<T> => {
  const message = `Invalid argument, we only support an array of strings`
  if (Array.isArray(items) === false) {
    throw new TypeError(message)
  }
  if (items.filter(maybe => typeof maybe !== 'string' || maybe === '').length > 0) {
    throw new TypeError(message)
  }
  const applicable = (items ?? []).filter(maybe => typeof maybe === 'string' && maybe === '')
  const mutableSet = new Set(applicable)
  const has = (maybe: T | unknown) => mutableSet.has(maybe as string)
  const keys = (): IterableIterator<T> => mutableSet.keys.bind(mutableSet)
  const iterator = (): IterableIterator<T> => mutableSet[Symbol.iterator].bind(mutableSet)

  return {
    [Symbol.iterator]: iterator,
    forEach: mutableSet.forEach.bind(mutableSet),
    entries: mutableSet.entries.bind(mutableSet),
    has,
    keys,
    values: keys,
    size: mutableSet.size,
  } as ReadonlySet<T>
}

Load from static files

initial

/**
 * Load data from static files.
 *
 * ```ts
 * // JSON files
 * import { loadAllJsonFiles, loadAllFilesAsOneString } from 'file-loading'
 * export const users = loadAllJsonFiles(/^user.*\.json$/, 'fixtures')
 *
 * // Stitch GraphQL
 * const typeDefs = `
 * ${loadAllFilesAsOneString(/\.graphql$/, 'schema').join('\n')}
 * type Query {
 *   hello(name: String): String!
 *   users: [User]!
 * }
 * `
 * ```
 */


import { readdirSync, statSync, readFileSync } from 'fs'
import { join } from 'path'
import * as loadJsonFile from 'load-json-file'

/**
 * Find files and return only ones with contents matching name
 */
const getFilePathsFinder = (
  fileNameRegExp: RegExp,
  fullPath: string,
): string[] => {
  const files: string[] = []

  const directoryExists = statSync(fullPath).isDirectory()
  if (directoryExists) {
    const fileNames: string[] = readdirSync(fullPath, { encoding: 'utf-8' })
      .filter(fileName => fileNameRegExp.test(fileName))
      .map(fileName => join(fullPath, fileName))
    files.push(...fileNames)
  }

  return files
}

export const getFilePaths = (
  fileNameRegExp: RegExp,
  relativeDirName?: string,
): string[] => {
  const fullPath = relativeDirName
    ? join(__dirname, relativeDirName)
    : join(__dirname)
  // console.log('getFilePaths', { relativeDirName, fullPath })
  const files = getFilePathsFinder(fileNameRegExp, fullPath)
  return files
}

export const loadAllJsonFiles = (pattern: RegExp, relativeDirName: string) =>
  (() => {
    const files = getFilePaths(pattern, relativeDirName)
    const parsed: any[] = []
    for (const filePath of files) {
      try {
        const contents = loadJsonFile.sync(filePath)
        if (Array.isArray(contents)) {
          parsed.push(...contents)
        }
      } catch (e) {
        console.log('loadAllFiles loading error', e)
      }
    }

    return parsed
  })()

export const loadAllFilesAsOneString = (
  pattern: RegExp,
  relativeDirName: string,
) =>
  (() => {
    const files = getFilePaths(pattern, relativeDirName)
    // console.log('loadAllFilesAsOneString', files)
    const contents: string[] = []
    for (const filePath of files) {
      try {
        const fileContents = readFileSync(filePath, { encoding: 'utf-8' })
        contents.push(fileContents)
      } catch (e) {
        console.log('loadAllFiles loading error', e)
      }
    }

    return contents
  })()

LayoutVariant

Parts of the mechanics to observe the current window size and tell a different variant. Here only one variant (either "mobile" or "desktop"), but could be adapted and configurable to not be bound to a framework

source from PR on vue-admin-template

const WIDTH = 992 // refer to Bootstrap's responsive design

export function onMounted() {
  const hasMethod = Reflect.has(this, 'checkIfMobile')
  const hasResizeHandler = Reflect.has(this, 'onResize')
  const hasEl = Reflect.has(this, '$el')
  if (hasMethod && hasResizeHandler && hasEl) {
    const isMobile = this.checkIfMobile()
    const { defaultView } = this.$el.ownerDocument
    const handler = this.onResize
    const layoutVariant = isMobile ? 'mobile' : 'desktop'
    this.layoutVariant = layoutVariant
    this.$emit('layout-variant', layoutVariant)
    defaultView.addEventListener('resize', handler)
    if (isMobile) {
      this.$emit('layout-sidebar', { opened: false, withoutAnimation: true })
    }
  } else {
    const message = `Please make sure you attach onMounted event handler to a Vue mounted lifecycle hook`
    throw new Error(message)
  }
}

export function checkIfMobile() {
  const hasEl = Reflect.has(this, '$el')
  const mobileBreakPoint = this.mobileBreakPoint ? this.mobileBreakPoint : WIDTH
  let isMobile = false
  if (hasEl) {
    const $el = this.$el
    let bodyRectWidth = mobileBreakPoint
    const hasOwnerDocument = 'ownerDocument' in $el
    let hasMethodName = false
    if (hasOwnerDocument) {
      const { body } = $el.ownerDocument
      hasMethodName = 'getBoundingClientRect' in body
      if (hasMethodName) {
        const bodyRect = body.getBoundingClientRect()
        bodyRectWidth = bodyRect.width
      }
    }
    isMobile = bodyRectWidth - 1 < mobileBreakPoint

    return isMobile
  } else {
    const message = `Please make sure you attach checkIfMobile to a Vue component as a method`
    throw new Error(message)
  }
}

/** @type {import('vue').VueConstructor} */
const main = {
  data() {
    const layoutVariant = 'desktop'
    const layoutIsHidden = false
    return {
      layoutVariant,
      layoutIsHidden
    }
  },
  props: {
    mobileBreakPoint: {
      type: Number,
      default: WIDTH
    },
    checkIfMobile: {
      type: Function,
      default() {
        return checkIfMobile.call(this)
      }
    }
  },
  computed: {
    isMobile() {
      const layoutVariant = this.layoutVariant
      return layoutVariant === 'mobile'
    }
  },
  async beforeDestroy() {
    const handler = this.onResize
    const $el = this.$el
    await this.$nextTick(async() => {
      const { defaultView } = $el.ownerDocument
      defaultView.removeEventListener('resize', handler)
    })
  },
  async mounted() {
    await this.$nextTick(onMounted.bind(this))
  },
  methods: {
    onResize() {
      const { hidden = false } = this.$el.ownerDocument
      const isMobile = this.checkIfMobile(this)
      const layoutVariant = isMobile ? 'mobile' : 'desktop'
      const layoutVariantChanged = layoutVariant !== this.layoutVariant
      this.layoutVariant = layoutVariant
      if (!hidden && layoutVariantChanged) {
        this.$emit('layout-variant', layoutVariant)
        this.$emit('layout-navbar', {
          fixed: isMobile
        })
        this.$emit('layout-sidebar', {
          opened: !isMobile,
          withoutAnimation: true
        })
      }
    }
  }
}

export default main
@renoirb
Copy link
Author

renoirb commented Nov 10, 2021

Serializing and DeSerializing String of boolean

Assuming getFromSessionStorage and setToSessionStorage only does read and write to localStorage or sessionStorage as a string to the key hasUserBeenPromptedKey that's configurable.

export type IBooleanAsString = 'true' | 'false'

export const assertIsStringWithBoolean: (input: unknown) => asserts input is IBooleanAsString = input => {
  assert(typeof input === 'string', `Received string "${input} was not a string`)
  assert(/^(true|false)$/.test(input), `Received string "${input} is not the string "true" or "false"`)
}

export class StringBooleanSerializationAdapter {
  static deserialize(input: string): boolean {
    let outcome = false
    assertIsStringWithBoolean(input)
    if (input === 'false') {
      outcome = false
    } else {
      outcome = input === 'true'
    }

    return outcome
  }
  static serialize(input: boolean): IBooleanAsString {
    const converted = String(input)
    assertIsStringWithBoolean(converted)
    return converted as IBooleanAsString
  }
}

export interface IValueDataStorageAdapter<T> {
  /**
   * The key in which the value will be stored in
   */
  readonly key: string
  /**
   * The appropriately deserialized value parsed from storage
   */
  readonly value: T
  /**
   * Make a read into storage for the value at key
   */
  check(): T
  /**
   * Mutate the storage to that new value
   */
  write(value: T): void
}

export class HasBeenPromptedStorageAdapter implements IValueDataStorageAdapter<boolean> {
  private readonly _storageKey: string

  private _storageContents: string | '' = ''

  get value(): boolean {
    return StringBooleanSerializationAdapter.deserialize(this._storageContents)
  }

  get key(): string {
    return this._storageKey
  }

  constructor(storageKey: string) {
    this._storageKey = storageKey
    this.check()
  }

  /**
   * @throws AssertionError - When storage service stored with an unacceptable string
   */
  check(): boolean {
    const received = getFromSessionStorage(this._storageKey) ?? ''
    let outcome = false
    // This is deliberate that if this._storageContents is '', that deserialize throws
    // so that we can catch when calling check
    try {
      outcome = StringBooleanSerializationAdapter.deserialize(received)
      // Do not mutate private if invalid
      this._storageContents = received
    } catch (e) {
      const message = `${e} in SessionStorage at key ${this._storageKey}`
      throw new AssertionError(message)
    }
    return outcome
  }

  write(value: boolean): void {
    const persisting = StringBooleanSerializationAdapter.serialize(value)
    setToSessionStorage(this._storageKey, persisting)
    this._storageContents = persisting
  }
}

@renoirb
Copy link
Author

renoirb commented Nov 25, 2021

Intl NumberFormat and DateTimeFormat

/**
 * Supported Locales countries (some of)
 *
 * @type {string[]}
 */
const LOCALES = [
  'fr-CA',
  'fr-FR',
  'ja-JP',
  'en-US',
  'en-GB',
  'ar-EG',
  'de-DE',
  'de-CH',
  'en-AU',
]

/**
 * Supported Unicode Calendar (some of)
 *
 * In [MDN Intl docs][MDN_Intl] locales, the "ca" part.
 * See also [MDN Intl.Locale calendar][MDN_Intl_Locale]
 *
 * [MDN_Intl_Locale]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar#unicode_calendar_keys
 * [MDN_Intl]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#parameters
 *
 * @type {string[]}
 */
const UNICODE_CALENDARS = [
  // Traditional Chinese calendar
  'chinese',
  // Coptic calendar
  'coptic',
  // Traditional Korean calendar
  'dangi',
  // Ethiopic calendar, Amete Alem (epoch approx. 5493 B.C.E)
  'ethioaa',
  // Ethiopic calendar, Amete Mihret (epoch approx, 8 C.E.)
  'ethiopic',
  // Gregorian calendar
  'gregory',
  // Traditional Hebrew calendar
  'hebrew',
  // Indian calendar
  'indian',
  // Islamic calendar
  'islamic',
  // Islamic calendar, Umm al-Qura
  'islamic-umalqura',
  // Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - astronomical epoch)
  'islamic-tbla',
  // Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - civil epoch)
  'islamic-civil',
  // Islamic calendar, Saudi Arabia sighting
  'islamic-rgsa',
  // ISO calendar (Gregorian calendar using the ISO 8601 calendar week rules)
  'iso8601',
  // Japanese Imperial calendar
  'japanese',
  // Persian calendar
  'persian',
  // Civil (algorithmic) Arabic calendar
  'roc',
  // Civil (algorithmic) Arabic calendar
  'islamicc',
  // Thai Buddhist calendar
  'buddhist',
] as const

/**
 * Supported Unicode numerals (some of)
 *
 * Partial list from [MDN Intl NumberFormat][MDN_Intl_NumberFormat], some more are listed in [MDN’s numberingSystem][MDN_Intl_numberingSystem]
 * and in the [Unicode specification][Unicode_tr35_numbers].
 *
 * As for Japanese and Chinese, [they share the same numeral system][Japanese_And_Chinese_numeral]
 *
 * [MDN_Intl]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#parameters
 * [MDN_Intl_NumberFormat]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
 * [MDN_Intl_numberingSystem]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numberingSystem
 * [Unicode_tr35_numbers]: https://www.unicode.org/reports/tr35/tr35-numbers.html#Contents
 * [Japanese_And_Chinese_numeral]: https://en.wikipedia.org/wiki/Japanese_numerals
 */
const UNICODE_NUMERALS = [
  'latn',
  'hanidec',
  'thai',
  // Personally never tried are below
  'adlm',
  'ahom',
  'arab',
  'arabext',
  'bali',
  'beng',
  'bhks',
  'brah',
  'cakm',
  'cham',
  'deva',
  'diak',
  'fullwide',
  'gong',
  'gonm',
  'gujr',
  'guru',
  'hmng',
  'hmnp',
  'java',
  'kali',
  'khmr',
  'knda',
  'lana',
  'lanatham',
  'laoo',
  'lepc',
  'limb',
  'mathbold',
  'mathdbl',
  'mathmono',
  'mathsanb',
  'mathsans',
  'mlym',
  'modi',
  'mong',
  'mroo',
  'mtei',
  'mymr',
  'mymrshan',
  'mymrtlng',
  'newa',
  'nkoo',
  'olck',
  'orya',
  'osma',
  'rohg',
  'saur',
  'segment',
  'shrd',
  'sind',
  'sinh',
  'sora',
  'sund',
  'takr',
  'talu',
  'tamldec',
  'telu',
  'tibt',
  'tirh',
  'vaii',
  'wara',
  'wcho',
] as const

/**
 * The Numbering systems for Intl.NumberFormat.
 *
 * Somehow (to be studied), the following entries [in numberingSystem][MDN_Intl_numberingSystem] does not exactly match the same as
 * Numerals.
 *
 * [MDN_Intl_numberingSystem]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numberingSystem
 */
const UNICODE_NUMBERING_SYSTEMS = [
  // Adlam digits
  'adlm',
  // Ahom digits
  'ahom',
  // Arabic-Indic digits
  'arab',
  // Extended Arabic-Indic digits
  'arabext',
  // Armenian upper case numerals — algorithmic
  'armn',
  // Armenian lower case numerals — algorithmic
  'armnlow',
  // Balinese digits
  'bali',
  // Bengali digits
  'beng',
  // Bhaiksuki digits
  'bhks',
  // Brahmi digits
  'brah',
  // Chakma digits
  'cakm',
  // Cham digits
  'cham',
  // Cyrillic numerals — algorithmic
  'cyrl',
  // Devanagari digits
  'deva',
  // Ethiopic numerals — algorithmic
  'ethi',
  // Financial numerals — may be algorithmic
  'finance',
  // Full width digits
  'fullwide',
  // Georgian numerals — algorithmic
  'geor',
  // Gunjala Gondi digits
  'gong',
  // Masaram Gondi digits
  'gonm',
  // Greek upper case numerals — algorithmic
  'grek',
  // Greek lower case numerals — algorithmic
  'greklow',
  // Gujarati digits
  'gujr',
  // Gurmukhi digits
  'guru',
  // Han-character day-of-month numbering for lunar/other traditional calendars
  'hanidays',
  // Positional decimal system using Chinese number ideographs as digits
  'hanidec',
  // Simplified Chinese numerals — algorithmic
  'hans',
  // Simplified Chinese financial numerals — algorithmic
  'hansfin',
  // Traditional Chinese numerals — algorithmic
  'hant',
  // Traditional Chinese financial numerals — algorithmic
  'hantfin',
  // Hebrew numerals — algorithmic
  'hebr',
  // Pahawh Hmong digits
  'hmng',
  // Nyiakeng Puachue Hmong digits
  'hmnp',
  // Javanese digits
  'java',
  // Japanese numerals — algorithmic
  'jpan',
  // Japanese financial numerals — algorithmic
  'jpanfin',
  // Japanese first-year Gannen numbering for Japanese calendar
  'jpanyear',
  // Kayah Li digits
  'kali',
  // Khmer digits
  'khmr',
  // Kannada digits
  'knda',
  // Tai Tham Hora (secular) digits
  'lana',
  // Tai Tham (ecclesiastical) digits
  'lanatham',
  // Lao digits
  'laoo',
  // Latin digits
  'latn',
  // Lepcha digits
  'lepc',
  // Limbu digits
  'limb',
  // Mathematical bold digits
  'mathbold',
  // Mathematical double-struck digits
  'mathdbl',
  // Mathematical monospace digits
  'mathmono',
  // Mathematical sans-serif bold digits
  'mathsanb',
  // Mathematical sans-serif digits
  'mathsans',
  // Malayalam digits
  'mlym',
  // Modi digits
  'modi',
  // Mongolian digits
  'mong',
  // Mro digits
  'mroo',
  // Meetei Mayek digits
  'mtei',
  // Myanmar digits
  'mymr',
  // Myanmar Shan digits
  'mymrshan',
  // Myanmar Tai Laing digits
  'mymrtlng',
  // Native digits
  'native',
  // Newa digits
  'newa',
  // N'Ko digits
  'nkoo',
  // Ol Chiki digits
  'olck',
  // Oriya digits
  'orya',
  // Osmanya digits
  'osma',
  // Hanifi Rohingya digits
  'rohg',
  // Roman upper case numerals — algorithmic
  'roman',
  // Roman lowercase numerals — algorithmic
  'romanlow',
  // Saurashtra digits
  'saur',
  // Sharada digits
  'shrd',
  // Khudawadi digits
  'sind',
  // Sinhala Lith digits
  'sinh',
  // Sora_Sompeng digits
  'sora',
  // Sundanese digits
  'sund',
  // Takri digits
  'takr',
  // New Tai Lue digits
  'talu',
  // Tamil numerals — algorithmic
  'taml',
  // Modern Tamil decimal digits
  'tamldec',
  // Telugu digits
  'telu',
  // Thai digits
  'thai',
  // Tirhuta digits
  'tirh',
  // Tibetan digits
  'tibt',
  // Traditional numerals — may be algorithmic
  'traditio',
  // Vai digits
  'vaii',
  // Warang Citi digits
  'wara',
  // Wancho digits
  'wcho',
] as const

type IUnicodeCalendar = typeof UNICODE_CALENDARS[number]
type IUnicodeNumberals = typeof UNICODE_NUMERALS[number]
type IUnicodeNumberingSystem = typeof UNICODE_NUMBERING_SYSTEMS[number]

const isUnicodeCalendar = (
  input: string | unknown,
): input is IUnicodeCalendar =>
  UNICODE_CALENDARS.includes(input as IUnicodeCalendar)

const isUnicodeNumeral = (
  input: string | unknown,
): input is IUnicodeNumberals =>
  UNICODE_NUMERALS.includes(input as IUnicodeNumberals)

const isUnicodeNumberingSystem = (
  input: string | unknown,
): input is IUnicodeNumberingSystem =>
  UNICODE_NUMBERING_SYSTEMS.includes(input as IUnicodeNumberingSystem)

/** ----------------------------- Function that relies on data above goes under this line ----------------------------- */

const isOnlyLanguageTag = (locale) =>
  typeof locale === 'string' &&
  locale.charAt(2) === '-' &&
  locale
    .replace('-', '')
    .split('')
    .map((l) => /[a-z]/i.test(l))
    .every((t) => t === true)

const assertOnlyLanguageTag = (input) => {
  if (!isOnlyLanguageTag(input)) {
    const message = `Unexpected input "${input}", we expected only a language tag without unicode additions such as "fr-CA"`
    throw new TypeError(message)
  }
}

const assertUnicodeCalendar: (
  input: unknown,
) => asserts input is IUnicodeCalendar = (input) => {
  if (!isUnicodeCalendar(input)) {
    const message = `Invalid unicode calendar symbol ${input}`
    throw new TypeError(message)
  }
}

const assertUnicodeNumeral: (
  input: unknown,
) => asserts input is IUnicodeNumberals = (input) => {
  if (!isUnicodeNumeral(input)) {
    const message = `Invalid numeral symbol ${input}`
    throw new TypeError(message)
  }
}

const assertUnicodeNumberingSystem: (
  input: unknown,
) => asserts input is IUnicodeNumberingSystem = (input) => {
  if (!isUnicodeNumberingSystem(input)) {
    const message = `Invalid numering system name ${input}`
    throw new TypeError(message)
  }
}

const withNumeralLanguageTag = (
  locale: string,
  numeral: IUnicodeNumberals = 'latn',
) => {
  assertUnicodeNumeral(numeral)
  assertOnlyLanguageTag(locale)
  const rest = ['u', 'nu', numeral].join('-')
  return `${locale}-${rest}`
}

const withCalendarLanguageTag = (
  locale: string,
  calendar: IUnicodeCalendar = 'gregory',
) => {
  assertUnicodeCalendar(calendar)
  assertOnlyLanguageTag(locale)
  const rest = ['u', 'ca', calendar].join('-')
  return `${locale}-${rest}`
}

/**
 * Initializes a configured Intl.DateTimeFormat
 *
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
 *
 * @returns {Intl.DateTimeFormat}
 */
const createDateTimeFormat = (
  locale: string,
  calendar: IUnicodeCalendar = 'gregory',
  numberingSystem: IUnicodeNumberingSystem = 'latn',
  options: Intl.DateTimeFormatOptions = {},
): ReturnType<typeof Intl.DateTimeFormat> => {
  const localeString = withCalendarLanguageTag(locale, calendar)
  const opts = { numberingSystem, ...options }
  if ('numberingSystem' in opts) {
    assertUnicodeNumberingSystem(opts.numberingSystem)
  }
  return new Intl.DateTimeFormat(localeString, opts)
}

/**
 * Initializes a configured Intl.NumberFormat
 *
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
 *
 * @returns {Intl.NumberFormat}
 */
const createNumberFormat = (
  locale: string,
  numeral: IUnicodeNumberals,
): ReturnType<typeof Intl.NumberFormat> => {
  const localeString = withNumeralLanguageTag(locale, numeral)
  return new Intl.NumberFormat(localeString)
}

EXAMPLES

NumberFormat

> var nf = createNumberFormat('fr-CA')
> nf.format('111111')
'111 111'
> nf.format(111111)
'111 111'
> nf.format(111_111)
'111 111'

> var nf = createNumberFormat('de-DE', 'latn')
> nf.format(111_111)
'111.111'

> var nf = createNumberFormat('fr-CA', 'thai')
> nf.format(111_111)
'๑๑๑ ๑๑๑'

> var nf = createNumberFormat('de-DE', 'thai')
> nf.format(111_111)
'๑๑๑.๑๑๑'
> nf.format(123_456)
'๑๒๓.๔๕๖'

> var nf = createNumberFormat('de-DE', 'java')
> nf.format(111_111)
'꧑꧑꧑.꧑꧑꧑'
> nf.format(123_456)
'꧑꧒꧓.꧔꧕꧖'

DateTimeFormat

var dt = createDateTimeFormat('fr-CA', 'gregory')
var dt = createDateTimeFormat('fr-CA')

var usedOptions = dt.resolvedOptions()
usedOptions.numberingSystem
> 'latn'

var dt = createDateTimeFormat('fr-CA', 'buddhist')
dt.format(Date.now())
> 'EB 2564-11-25'

var dt = createDateTimeFormat('fr-CH', 'japanese')
dt.format(Date.now())
> '25/11/3 R'


var dt = createDateTimeFormat('fr-CA', 'gregory', 'latn', { month: 'long' })
dt.format(Date.now())
> février

var dt = createDateTimeFormat('fr-CA', 'gregory', 'latn', { dateStyle: 'full' })
dt.format(Date.now())
> mercredi 16 février 2022

Figuring out difference between numbering system and numerals

var foo = UNICODE_NUMBERING_SYSTEMS.filter(value => UNICODE_NUMERALS.includes(value) === false ? value : undefined)
> foo
[
  'armn',     'armnlow',  'cyrl',
  'ethi',     'finance',  'geor',
  'grek',     'greklow',  'hanidays',
  'hans',     'hansfin',  'hant',
  'hantfin',  'hebr',     'jpan',
  'jpanfin',  'jpanyear', 'native',
  'roman',    'romanlow', 'taml',
  'traditio'
]
var foo = UNICODE_NUMERALS.filter(value => UNICODE_NUMBERING_SYSTEMS.includes(value) === false ? value : undefined);
> foo
[ 'diak', 'segment' ]

@renoirb
Copy link
Author

renoirb commented Dec 17, 2021

When NODE_OPTIONS max-old-space

const hasMaxOldSpace = process.env.NODE_OPTIONS.split(' ')
  .map((c) => c.replace(/^--/, ''))
  .map((c) => c.split('=')[0])
  .includes('max-old-space-size')
if (!hasMaxOldSpace) {
  process.env.NODE_OPTIONS = [
    process.env.NODE_OPTIONS,
    '--max-old-space-size=8192',
  ].join(' ')
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment