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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | 2x 2x 2x 33x 33x 30x 30x 33x 6x 6x 24x 24x 33x 33x 33x 1x 1x 33x 33x 1x 1x 21x 21x 2x 75x 75x 2x 9x 8x 8x 6x 2x 9x | /**
* A number of contacts, as a reader types it and as the app says it (§6.5).
*
* One parser for the breadcrumb's *contacts* box and for `chromosomix
* --contacts`, so a number that works in one works in the other: `10M`,
* `23,000,000`, `2.5e7`, `500k`, `1.2G` — the forms a methods section and a
* `cooler info` print. And one phrase for what a resampled map is, so the
* caption of a figure, the heading of a cell and a task's framing line say it
* the same way.
*/
/** What a suffix multiplies by: thousand, million, billion (`G` or `B`). */
const SUFFIX: Record<string, number> = { k: 1e3, m: 1e6, g: 1e9, b: 1e9 }
/** Separators a number is grouped with: commas, underscores, spaces, thin spaces, apostrophes. */
const GROUPING = /[\s,_' ]/g
/**
* A total typed by a reader: `null` for an empty box (the file as stored), a
* number of contacts, or why it is not one.
*/
export function parseContacts(text: string): number | null | { refused: string } {
const trimmed = text.trim()
if (trimmed === '') return null
const compact = trimmed.replace(GROUPING, '')
const match = /^(\d+(?:\.\d*)?|\.\d+)(?:e([+-]?\d+))?([kmgb])?$/i.exec(compact)
if (match === null) {
return { refused: `“${trimmed}” is not a number of contacts: type one like 10M or 23,000,000.` }
}
const [, digits, exponent, suffix] = match
const value =
Number(digits) * 10 ** Number(exponent ?? 0) * (suffix === undefined ? 1 : SUFFIX[suffix.toLowerCase()])
// `1.5k` is 1,500 and `0.1` is not a contact; rounding off a hair of float
// (`1.1M` is 1100000.0000000002) is not rounding off a contact.
const rounded = Math.round(value)
if (!Number.isFinite(value) || Math.abs(value - rounded) > 1e-6 * Math.max(1, value)) {
return { refused: `“${trimmed}” is not a whole number of contacts.` }
}
if (rounded < 1) return { refused: 'A map needs at least one contact.' }
if (rounded > Number.MAX_SAFE_INTEGER) {
return { refused: `“${trimmed}” contacts is past what can be counted exactly here.` }
}
return rounded
}
/** `179,175,551` — every digit, grouped, as the box and a caption print it. */
export function contactsText(contacts: number): string {
return Math.round(contacts).toLocaleString('en-US')
}
/** What a map was drawn to, as the reader reads it back. */
export interface DrawnDepth {
/** The total it was drawn to. */
contacts: number
/** The contacts the file holds. */
total: number
/** `none` where the two are the same number. */
mode: 'none' | 'sub' | 'over'
}
/**
* `subsampled to 10,000,000 contacts (of 179,175,551)` — or oversampled, with
* Boost-HiC named, since an oversample is an inference and a subsample is not.
* Empty for a map drawn to its own total.
*/
export function depthPhrase(depth: DrawnDepth): string {
if (depth.mode === 'none') return ''
const of = `of ${contactsText(depth.total)}`
return depth.mode === 'sub'
? `subsampled to ${contactsText(depth.contacts)} contacts (${of})`
: `oversampled to ${contactsText(depth.contacts)} contacts (${of}, Boost-HiC)`
}
|