All files / src format.ts

100% Statements 24/24
75% Branches 18/24
100% Functions 3/3
100% Lines 24/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58                                    2x 1585x 1585x 1585x 1585x 1585x 1585x 1585x 1585x 1585x 1585x 1585x 1585x 1585x 1585x     2x 125x 125x                       2x 27x     27x 12x 27x 27x  
/**
 * Python-flavoured value formatting for error messages.
 *
 * The reader mirrors metacooler's errors, and metacooler builds its messages
 * with `{value!r}`. Reproducing `repr()` rather than reaching for
 * `JSON.stringify` keeps the JS and Python wordings byte-identical for the
 * common cases (`attrs['bin-size'] must be an integer, got 'null'`), which is
 * what makes a message greppable across both implementations.
 */
 
/**
 * Render a value the way Python's `repr()` would.
 *
 * Strings get single quotes unless they contain one and no double quote —
 * exactly CPython's rule. Booleans are `True`/`False` because that is what an
 * h5py attribute prints as, and `null`/`undefined` are `None` because that is
 * what an absent attribute is on the Python side.
 */
export function pyRepr(value: unknown): string {
  if (value === null || value === undefined) return 'None'
  if (typeof value === 'boolean') return value ? 'True' : 'False'
  if (typeof value === 'number') return Number.isInteger(value) ? String(value) : String(value)
  if (Array.isArray(value)) return `[${value.map(pyRepr).join(', ')}]`
  if (typeof value !== 'string') return String(value)
  const quote = value.includes("'") && !value.includes('"') ? '"' : "'"
  const escaped = value
    .replace(/\\/g, '\\\\')
    .replace(/\n/g, '\\n')
    .replace(/\r/g, '\\r')
    .replace(/\t/g, '\\t')
    .replace(new RegExp(quote, 'g'), `\\${quote}`)
  return `${quote}${escaped}${quote}`
}
 
/** Render a list of strings as Python would print it: `['a', 'b']`. */
export function pyList(values: readonly string[]): string {
  return `[${values.map(pyRepr).join(', ')}]`
}
 
/**
 * Parse a string the way Python's `int()` does: surrounding whitespace is
 * ignored, a leading sign is allowed, single underscores may separate digits
 * (`int('1_000') == 1000`), and *nothing else* is — not `'1e3'`, not
 * `'1000.0'`, not `'0x10'`.
 *
 * Returns `null` when the string is not an integer literal, so callers decide
 * whether that is an error (`_int_attr`) or a value to leave alone
 * (`MetaCooler.info`'s suppressed cast).
 */
export function pyInt(text: string): number | null {
  const trimmed = text.trim()
  // Underscores are legal only *between* digits, which is why this is not a
  // strip-then-match: `int('1__0')`, `int('_1')` and `int('1_')` all raise.
  if (!/^[+-]?[0-9](?:_?[0-9])*$/.test(trimmed)) return null
  const value = Number(trimmed.replace(/_/g, ''))
  return Number.isFinite(value) ? value : null
}