linked-rolls 0.42.0 → 0.44.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.
@@ -0,0 +1,121 @@
1
+ import { quantity } from "./Quantity";
2
+ /**
3
+ * The statistics a sample of measurements is described with.
4
+ *
5
+ * The centre and the scatter are taken from the median rather than from
6
+ * the mean, because the departures a measurement over an edition looks
7
+ * for are in the sample it is taken over. A mean and a standard
8
+ * deviation would grow towards those departures until they no longer
9
+ * stood out; a median and a median absolute deviation leave them out of
10
+ * account.
11
+ */
12
+ /** The middle of a sample, the mean of the two middle values where it has an even number of them. */
13
+ export const medianOf = (values) => {
14
+ if (values.length === 0)
15
+ return undefined;
16
+ const sorted = [...values].sort((a, b) => a - b);
17
+ const middle = Math.floor(sorted.length / 2);
18
+ return sorted.length % 2 === 1
19
+ ? sorted[middle]
20
+ : quantity((sorted[middle - 1] + sorted[middle]) / 2);
21
+ };
22
+ /** What the median absolute deviation must be multiplied by to estimate the standard deviation of a normal sample. */
23
+ const MAD_TO_SIGMA = 1.4826;
24
+ /** Where the sample sits and how far it scatters, or nothing for an empty sample. */
25
+ export const spreadOf = (values) => {
26
+ const median = medianOf(values);
27
+ if (median === undefined)
28
+ return undefined;
29
+ const deviations = values.map(value => quantity(Math.abs(value - median)));
30
+ return {
31
+ n: values.length,
32
+ median,
33
+ sigma: quantity(MAD_TO_SIGMA * (medianOf(deviations) ?? 0))
34
+ };
35
+ };
36
+ /** How far the value lies from the centre of the spread, in units of its scatter. */
37
+ export const standardise = (value, spread) => (value - spread.median) / spread.sigma;
38
+ /** The complementary error function, after Press et al., whose fractional error stays below 1.2e-7. */
39
+ const erfc = (x) => {
40
+ const z = Math.abs(x);
41
+ const t = 1 / (1 + z / 2);
42
+ const tail = t * Math.exp(-z * z - 1.26551223 + t * (1.00002368 + t * (0.37409196 + t * (0.09678418 +
43
+ t * (-0.18628806 + t * (0.27886807 + t * (-1.13520398 + t * (1.48851587 +
44
+ t * (-0.82215223 + t * 0.17087277)))))))));
45
+ return x >= 0 ? tail : 2 - tail;
46
+ };
47
+ /** The share of a normal sample lying below `z` standard deviations. */
48
+ export const normalBelow = (z) => erfc(-z / Math.SQRT2) / 2;
49
+ /** The share of a normal sample lying further than `z` standard deviations from its centre, on either side. */
50
+ export const normalBeyond = (z) => erfc(z / Math.SQRT2);
51
+ const horner = (coefficients, x) => coefficients.reduce((total, coefficient) => total * x + coefficient, 0);
52
+ const CENTRAL_NUMERATOR = [
53
+ -3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02,
54
+ 1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00
55
+ ];
56
+ const CENTRAL_DENOMINATOR = [
57
+ -5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02,
58
+ 6.680131188771972e+01, -1.328068155288572e+01
59
+ ];
60
+ const TAIL_NUMERATOR = [
61
+ -7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00,
62
+ -2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00
63
+ ];
64
+ const TAIL_DENOMINATOR = [
65
+ 7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00, 3.754408661907416e+00
66
+ ];
67
+ /** Outside this share the tail approximation is used in place of the central one. */
68
+ const CENTRAL_SHARE = 0.02425;
69
+ const tailQuantile = (share) => {
70
+ const q = Math.sqrt(-2 * Math.log(share));
71
+ return horner(TAIL_NUMERATOR, q) / (horner(TAIL_DENOMINATOR, q) * q + 1);
72
+ };
73
+ /**
74
+ * How many standard deviations out the given share of a normal sample
75
+ * lies below, after Acklam's rational approximation, whose relative
76
+ * error stays below 1.15e-9. The inverse of `normalBelow`.
77
+ */
78
+ export const normalQuantile = (share) => {
79
+ if (share <= 0)
80
+ return -Infinity;
81
+ if (share >= 1)
82
+ return Infinity;
83
+ if (share < CENTRAL_SHARE)
84
+ return tailQuantile(share);
85
+ if (share > 1 - CENTRAL_SHARE)
86
+ return -tailQuantile(1 - share);
87
+ const q = share - 0.5;
88
+ const r = q * q;
89
+ return horner(CENTRAL_NUMERATOR, r) * q / (horner(CENTRAL_DENOMINATOR, r) * r + 1);
90
+ };
91
+ /**
92
+ * How much heavier the tails of a standardised sample are than a normal
93
+ * sample's. Zero for a normal shape, positive where more of the sample
94
+ * lies far out than the curve allows.
95
+ */
96
+ export const excessKurtosisOf = (standardised) => standardised.reduce((total, z) => total + z ** 4, 0) / standardised.length - 3;
97
+ /** What the sample puts beyond the given distance, against what a normal sample would. */
98
+ export const tailOf = (standardised, beyond) => ({
99
+ beyond,
100
+ observed: standardised.filter(z => Math.abs(z) > beyond).length,
101
+ expected: standardised.length * normalBeyond(beyond)
102
+ });
103
+ /**
104
+ * The sample counted into bins of the given width, laid out on
105
+ * multiples of that width so that two histograms of one width share
106
+ * their bounds. Nothing for an empty sample or a width of nothing.
107
+ */
108
+ export const histogramOf = (values, binWidth) => {
109
+ if (values.length === 0 || binWidth <= 0)
110
+ return undefined;
111
+ const first = Math.floor(Math.min(...values) / binWidth) * binWidth;
112
+ const last = Math.ceil(Math.max(...values) / binWidth) * binWidth;
113
+ const bins = Math.max(1, Math.round((last - first) / binWidth));
114
+ return {
115
+ edges: Array.from({ length: bins + 1 }, (_, i) => quantity(first + i * binWidth)),
116
+ counts: values.reduce((tally, value) => {
117
+ tally[Math.min(bins - 1, Math.floor((value - first) / binWidth))] += 1;
118
+ return tally;
119
+ }, new Array(bins).fill(0))
120
+ };
121
+ };
package/lib/utils.d.ts CHANGED
@@ -14,6 +14,8 @@ export type WithId = {
14
14
  };
15
15
  /** Whether a value is a date as the format writes one, `YYYY-MM-DD`. */
16
16
  export declare const isDateString: (value: unknown) => value is string;
17
+ /** The items by the key each of them gives, every group in the order its first item came in. */
18
+ export declare const groupBy: <T>(items: readonly T[], keyOf: (item: T) => string) => Map<string, T[]>;
17
19
  export type WithNote = {
18
20
  /**
19
21
  * A free-text note providing additional context.
package/lib/utils.js CHANGED
@@ -1,2 +1,12 @@
1
1
  /** Whether a value is a date as the format writes one, `YYYY-MM-DD`. */
2
2
  export const isDateString = (value) => typeof value === 'string' && /^\d{4}-\d{1,2}-\d{1,2}$/.test(value);
3
+ /** The items by the key each of them gives, every group in the order its first item came in. */
4
+ export const groupBy = (items, keyOf) => items.reduce((groups, item) => {
5
+ const key = keyOf(item);
6
+ const group = groups.get(key);
7
+ if (group)
8
+ group.push(item);
9
+ else
10
+ groups.set(key, [item]);
11
+ return groups;
12
+ }, new Map());
@@ -0,0 +1,15 @@
1
+ import { Concept } from "./Agent";
2
+ /**
3
+ * The concepts the type vocabulary declares, whatever kind they are:
4
+ * the reproducing systems and the procedures. An edition names one of
5
+ * them by its IRI alone.
6
+ */
7
+ export declare const vocabulary: readonly Concept[];
8
+ /** The concept of that IRI, where the vocabulary declares one. */
9
+ export declare const conceptOf: (id: string | undefined) => Concept | undefined;
10
+ /**
11
+ * What a concept is called: the name it states, else the one the
12
+ * vocabulary gives it, else its IRI, so that a reader always has
13
+ * something to show.
14
+ */
15
+ export declare const nameOf: (concept: Concept) => string;
@@ -0,0 +1,20 @@
1
+ import { procedures } from "./procedures";
2
+ import { systemOf } from "./TrackerBar";
3
+ import { trackerBars } from "./systems";
4
+ /**
5
+ * The concepts the type vocabulary declares, whatever kind they are:
6
+ * the reproducing systems and the procedures. An edition names one of
7
+ * them by its IRI alone.
8
+ */
9
+ export const vocabulary = [
10
+ ...trackerBars.map(systemOf),
11
+ ...procedures
12
+ ];
13
+ /** The concept of that IRI, where the vocabulary declares one. */
14
+ export const conceptOf = (id) => id === undefined ? undefined : vocabulary.find(concept => concept.id === id);
15
+ /**
16
+ * What a concept is called: the name it states, else the one the
17
+ * vocabulary gives it, else its IRI, so that a reader always has
18
+ * something to show.
19
+ */
20
+ export const nameOf = (concept) => concept.name ?? conceptOf(concept.id)?.name ?? concept.id ?? '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "Digital editions of piano rolls: import, collation, editorial assumptions, JSON-LD export, and emulation through a reproducing system",
5
5
  "license": "MIT",
6
6
  "repository": {