cx 26.4.4 → 26.5.1

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.
Files changed (69) hide show
  1. package/build/charts/axis/Axis.d.ts +8 -0
  2. package/build/charts/axis/Axis.d.ts.map +1 -1
  3. package/build/charts/axis/Axis.js +18 -1
  4. package/build/charts/axis/TimeAxis.js +1 -0
  5. package/build/ui/Format.d.ts.map +1 -1
  6. package/build/ui/Format.js +26 -2
  7. package/build/util/Format.d.ts.map +1 -1
  8. package/build/util/Format.js +6 -0
  9. package/build/util/date/dateQuarter.d.ts +7 -0
  10. package/build/util/date/dateQuarter.d.ts.map +1 -0
  11. package/build/util/date/dateQuarter.js +8 -0
  12. package/build/util/date/dayBefore.d.ts +12 -0
  13. package/build/util/date/dayBefore.d.ts.map +1 -0
  14. package/build/util/date/dayBefore.js +15 -0
  15. package/build/util/date/index.d.ts +2 -0
  16. package/build/util/date/index.d.ts.map +1 -1
  17. package/build/util/date/index.js +2 -0
  18. package/build/widgets/form/DateTimePicker.d.ts.map +1 -1
  19. package/build/widgets/form/DateTimePicker.js +53 -31
  20. package/build/widgets/form/Field.d.ts.map +1 -1
  21. package/build/widgets/form/Field.js +2 -1
  22. package/build/widgets/form/Wheel.d.ts +8 -0
  23. package/build/widgets/form/Wheel.d.ts.map +1 -1
  24. package/build/widgets/form/Wheel.js +30 -7
  25. package/build/widgets/grid/Grid.d.ts +1 -1
  26. package/build/widgets/grid/Grid.d.ts.map +1 -1
  27. package/dist/charts.css +6 -0
  28. package/dist/charts.js +18 -1
  29. package/dist/manifest.js +787 -778
  30. package/dist/ui.js +33 -1
  31. package/dist/util.js +32 -0
  32. package/dist/widgets.js +225 -173
  33. package/package.json +1 -1
  34. package/src/charts/BarGraph.scss +31 -31
  35. package/src/charts/Legend.scss +57 -57
  36. package/src/charts/LegendEntry.scss +35 -35
  37. package/src/charts/LineGraph.scss +28 -28
  38. package/src/charts/RangeMarker.scss +3 -0
  39. package/src/charts/axis/Axis.tsx +31 -1
  40. package/src/charts/axis/TimeAxis.tsx +1 -0
  41. package/src/charts/helpers/SnapPointFinder.ts +136 -136
  42. package/src/charts/helpers/ValueAtFinder.ts +72 -72
  43. package/src/charts/index.scss +1 -0
  44. package/src/data/AugmentedViewBase.ts +89 -89
  45. package/src/data/View.ts +301 -301
  46. package/src/data/createAccessorModelProxy.ts +66 -66
  47. package/src/ui/Format.spec.ts +32 -0
  48. package/src/ui/Format.ts +27 -2
  49. package/src/ui/Repeater.spec.tsx +181 -181
  50. package/src/util/Format.spec.ts +11 -0
  51. package/src/util/Format.ts +7 -0
  52. package/src/util/date/dateQuarter.ts +8 -0
  53. package/src/util/date/dayBefore.ts +15 -0
  54. package/src/util/date/index.ts +2 -0
  55. package/src/util/scss/include.scss +69 -69
  56. package/src/widgets/Button.maps.scss +103 -103
  57. package/src/widgets/form/Calendar.tsx +772 -772
  58. package/src/widgets/form/DateTimePicker.tsx +453 -392
  59. package/src/widgets/form/Field.tsx +2 -1
  60. package/src/widgets/form/ValidationGroup.spec.tsx +30 -1
  61. package/src/widgets/form/Wheel.tsx +36 -7
  62. package/src/widgets/grid/Grid.scss +657 -657
  63. package/src/widgets/grid/Grid.tsx +1 -1
  64. package/src/widgets/grid/variables.scss +47 -47
  65. package/src/widgets/index.ts +63 -63
  66. package/src/widgets/nav/MenuItem.scss +150 -150
  67. package/src/widgets/nav/Tab.ts +122 -122
  68. package/src/widgets/overlay/Overlay.tsx +1029 -1029
  69. package/src/widgets/variables.scss +61 -61
@@ -1,66 +1,66 @@
1
- // Homomorphic mapped type preserves Go-to-Definition in IDEs
2
- // -? strips optionality, as clause filters conflicting method names
3
- type AccessorChainMap<M> = {
4
- [K in keyof M as Exclude<K, "toString" | "valueOf" | "nameOf">]-?: AccessorChain<M[K]>;
5
- };
6
-
7
- // Check if a type is `any` using the intersection trick
8
- type IsAny<T> = 0 extends 1 & T ? true : false;
9
-
10
- export type AccessorChain<M> = (IsAny<M> extends true
11
- ? { [key: string]: any } // Allow any property access for `any` type
12
- : [M] extends [object]
13
- ? AccessorChainMap<M> // Direct mapping preserves IDE navigation
14
- : [NonNullable<M>] extends [object]
15
- ? AccessorChainMap<NonNullable<M>> // Fallback for nullable types (e.g. optional properties)
16
- : {}) & {
17
- toString(): string;
18
- valueOf(): M | undefined;
19
- nameOf(): string;
20
- };
21
-
22
- const emptyFn = () => {};
23
-
24
- export function createAccessorModelProxy<M>(chain: string = ""): AccessorChain<M> {
25
- let lastOp: string | null = null;
26
-
27
- const proxy = new Proxy(emptyFn, {
28
- get: (_, name: string | symbol) => {
29
- if (typeof name !== "string") return proxy;
30
-
31
- switch (name) {
32
- case "isAccessorChain":
33
- return true;
34
-
35
- case "toString":
36
- case "valueOf":
37
- case "nameOf":
38
- lastOp = name;
39
- return proxy;
40
- }
41
-
42
- let newChain = chain;
43
- if (newChain.length > 0) newChain += ".";
44
- newChain += name;
45
- return createAccessorModelProxy(newChain);
46
- },
47
-
48
- apply(): string {
49
- switch (lastOp) {
50
- case "nameOf":
51
- const lastDotIndex = chain.lastIndexOf(".");
52
- return lastDotIndex > 0 ? chain.substring(lastDotIndex + 1) : chain;
53
-
54
- default:
55
- return chain;
56
- }
57
- },
58
- });
59
- return proxy as unknown as AccessorChain<M>;
60
- }
61
-
62
- export const createModel = createAccessorModelProxy;
63
-
64
- export function isAccessorChain<M>(value: unknown): value is AccessorChain<M> {
65
- return value != null && !!(value as any).isAccessorChain;
66
- }
1
+ // Homomorphic mapped type preserves Go-to-Definition in IDEs
2
+ // -? strips optionality, as clause filters conflicting method names
3
+ type AccessorChainMap<M> = {
4
+ [K in keyof M as Exclude<K, "toString" | "valueOf" | "nameOf">]-?: AccessorChain<M[K]>;
5
+ };
6
+
7
+ // Check if a type is `any` using the intersection trick
8
+ type IsAny<T> = 0 extends 1 & T ? true : false;
9
+
10
+ export type AccessorChain<M> = (IsAny<M> extends true
11
+ ? { [key: string]: any } // Allow any property access for `any` type
12
+ : [M] extends [object]
13
+ ? AccessorChainMap<M> // Direct mapping preserves IDE navigation
14
+ : [NonNullable<M>] extends [object]
15
+ ? AccessorChainMap<NonNullable<M>> // Fallback for nullable types (e.g. optional properties)
16
+ : {}) & {
17
+ toString(): string;
18
+ valueOf(): M | undefined;
19
+ nameOf(): string;
20
+ };
21
+
22
+ const emptyFn = () => {};
23
+
24
+ export function createAccessorModelProxy<M>(chain: string = ""): AccessorChain<M> {
25
+ let lastOp: string | null = null;
26
+
27
+ const proxy = new Proxy(emptyFn, {
28
+ get: (_, name: string | symbol) => {
29
+ if (typeof name !== "string") return proxy;
30
+
31
+ switch (name) {
32
+ case "isAccessorChain":
33
+ return true;
34
+
35
+ case "toString":
36
+ case "valueOf":
37
+ case "nameOf":
38
+ lastOp = name;
39
+ return proxy;
40
+ }
41
+
42
+ let newChain = chain;
43
+ if (newChain.length > 0) newChain += ".";
44
+ newChain += name;
45
+ return createAccessorModelProxy(newChain);
46
+ },
47
+
48
+ apply(): string {
49
+ switch (lastOp) {
50
+ case "nameOf":
51
+ const lastDotIndex = chain.lastIndexOf(".");
52
+ return lastDotIndex > 0 ? chain.substring(lastDotIndex + 1) : chain;
53
+
54
+ default:
55
+ return chain;
56
+ }
57
+ },
58
+ });
59
+ return proxy as unknown as AccessorChain<M>;
60
+ }
61
+
62
+ export const createModel = createAccessorModelProxy;
63
+
64
+ export function isAccessorChain<M>(value: unknown): value is AccessorChain<M> {
65
+ return value != null && !!(value as any).isAccessorChain;
66
+ }
@@ -0,0 +1,32 @@
1
+ import assert from "assert";
2
+ import { Format } from "./Format";
3
+
4
+ // The `quarter` formatter is registered eagerly when ui/Format is imported,
5
+ // so these tests do not need enableCultureSensitiveFormatting().
6
+ describe("Format - quarter", function () {
7
+ it("renders the calendar quarter with the default pattern", function () {
8
+ assert.equal(Format.value(new Date(2020, 0, 15), "quarter"), "Q1 2020");
9
+ assert.equal(Format.value(new Date(2020, 5, 1), "quarter"), "Q2 2020");
10
+ assert.equal(Format.value(new Date(2020, 11, 31), "quarter"), "Q4 2020");
11
+ });
12
+
13
+ it("supports custom patterns and the {yy} token", function () {
14
+ assert.equal(Format.value(new Date(2020, 0, 1), "quarter;{yy}Q{q}"), "20Q1");
15
+ assert.equal(Format.value(new Date(2020, 8, 1), "quarter;{q}Q{yyyy}"), "3Q2020");
16
+ });
17
+
18
+ it("accepts both lowercase and uppercase placeholders", function () {
19
+ assert.equal(Format.value(new Date(2020, 0, 1), "quarter;{YY}Q{Q}"), "20Q1");
20
+ assert.equal(Format.value(new Date(2020, 0, 1), "quarter;Q{Q} {YYYY}"), "Q1 2020");
21
+ });
22
+
23
+ it("treats the input as an exclusive range end with the exclusive flag", function () {
24
+ assert.equal(Format.value(new Date(2021, 0, 1), "quarter;{yy}Q{q};exclusive"), "20Q4");
25
+ assert.equal(Format.value(new Date(2021, 0, 1), "quarter;{yy}Q{q};ex"), "20Q4");
26
+ });
27
+
28
+ it("falls back to the default pattern when the pattern argument is empty (quarter;;e)", function () {
29
+ assert.equal(Format.value(new Date(2021, 0, 1), "quarter;;e"), "Q4 2020");
30
+ assert.equal(Format.value(new Date(2020, 0, 1), "quarter;;e"), "Q4 2019");
31
+ });
32
+ });
package/src/ui/Format.ts CHANGED
@@ -1,12 +1,31 @@
1
1
  import { Culture, getCurrentCultureCache } from "./Culture";
2
2
  import { Format as Fmt, resolveMinMaxFractionDigits, setGetFormatCacheCallback } from "../util/Format";
3
3
  import { setGetExpressionCacheCallback } from "../data/Expression";
4
- import { setGetStringTemplateCacheCallback } from "../data/StringTemplate";
5
- import { parseDateInvariant } from "../util";
4
+ import { setGetStringTemplateCacheCallback, StringTemplate } from "../data/StringTemplate";
5
+ import { dateQuarter, dayBefore, parseDateInvariant } from "../util";
6
6
  import { GlobalCacheIdentifier } from "../util/GlobalCacheIdentifier";
7
7
 
8
8
  export const Format = Fmt;
9
9
 
10
+ // The `quarter` formatter renders a calendar quarter via a string-template
11
+ // pattern with `{q}` (quarter number), `{yyyy}` and `{yy}` (year) placeholders.
12
+ // Placeholders are case-insensitive (`{Q}`, `{YYYY}`, `{YY}` work too). It lives
13
+ // here rather than in util/Format because it depends on StringTemplate from the
14
+ // data layer, which util/ cannot import. It is registered eagerly since it does
15
+ // not depend on culture settings.
16
+ Fmt.registerFactory("quarter", (fmt: any, pattern?: string, mode?: string) => {
17
+ let exclusive = mode === "exclusive" || mode === "ex" || mode === "e";
18
+ let template = StringTemplate.get(pattern || "Q{q} {yyyy}");
19
+ return (value: any) => {
20
+ let date = parseDateInvariant(value);
21
+ if (exclusive) date = dayBefore(date);
22
+ let q = dateQuarter(date);
23
+ let yyyy = date.getFullYear();
24
+ let yy = String(yyyy % 100).padStart(2, "0");
25
+ return template({ q, Q: q, yyyy, YYYY: yyyy, yy, YY: yy });
26
+ };
27
+ });
28
+
10
29
  let cultureSensitiveFormatsRegistered = false;
11
30
 
12
31
  export function resolveNumberFormattingFlags(flags?: string): any {
@@ -86,6 +105,12 @@ export function enableCultureSensitiveFormatting() {
86
105
  return (value: any) => formatter.format(parseDateInvariant(value));
87
106
  });
88
107
 
108
+ Fmt.registerFactory(["dayBefore", "daybefore"], (fmt: any, format = "yyyyMd hhmm") => {
109
+ let culture = Culture.getDateTimeCulture();
110
+ let formatter = culture.getFormatter(format);
111
+ return (value: any) => formatter.format(dayBefore(parseDateInvariant(value)));
112
+ });
113
+
89
114
  setGetFormatCacheCallback(() => {
90
115
  let cache = getCurrentCultureCache();
91
116
  if (!cache.formatCache) cache.formatCache = {};
@@ -1,181 +1,181 @@
1
- import { Store } from "../data/Store";
2
- import { Repeater } from "./Repeater";
3
- import { bind } from "./bind";
4
- import { createTestRenderer, act } from "../util/test/createTestRenderer";
5
- import { createAccessorModelProxy } from "../data/createAccessorModelProxy";
6
-
7
- import assert from "assert";
8
-
9
- describe("Repeater", () => {
10
- it("allows sorting", async () => {
11
- let data = [
12
- {
13
- value: "C",
14
- },
15
- {
16
- value: "B",
17
- },
18
- {
19
- value: "A",
20
- },
21
- ];
22
-
23
- let widget = (
24
- <cx>
25
- <div>
26
- <Repeater records={data} sorters={[{ field: "value", direction: "ASC" }]} recordAlias="$item">
27
- <div text={bind("$item.value")} />
28
- </Repeater>
29
- </div>
30
- </cx>
31
- );
32
-
33
- let store = new Store();
34
-
35
- const component = await createTestRenderer(store, widget);
36
-
37
- let tree = component.toJSON();
38
- assert.deepEqual(tree, {
39
- type: "div",
40
- props: {},
41
- children: [
42
- {
43
- type: "div",
44
- props: {},
45
- children: ["A"],
46
- },
47
- {
48
- type: "div",
49
- props: {},
50
- children: ["B"],
51
- },
52
- {
53
- type: "div",
54
- props: {},
55
- children: ["C"],
56
- },
57
- ],
58
- });
59
- });
60
-
61
- it("changes are properly updated", async () => {
62
- let divInstances: any[] = [];
63
- let widget = (
64
- <cx>
65
- <div>
66
- <Repeater records={bind("data")} sorters={[{ field: "value", direction: "ASC" }]} recordAlias="$item">
67
- <div
68
- text={bind("$item.value")}
69
- onExplore={(context, instance) => {
70
- divInstances.push(instance);
71
- }}
72
- />
73
- </Repeater>
74
- </div>
75
- </cx>
76
- );
77
-
78
- let store = new Store({
79
- data: {
80
- data: [
81
- {
82
- value: "C",
83
- },
84
- {
85
- value: "B",
86
- },
87
- ],
88
- },
89
- });
90
-
91
- const component = await createTestRenderer(store, widget);
92
-
93
- let tree = component.toJSON();
94
- assert.deepEqual(tree, {
95
- type: "div",
96
- props: {},
97
- children: [
98
- {
99
- type: "div",
100
- props: {},
101
- children: ["B"],
102
- },
103
- {
104
- type: "div",
105
- props: {},
106
- children: ["C"],
107
- },
108
- ],
109
- });
110
-
111
- divInstances = [];
112
-
113
- await act(async () => {
114
- store.update("data", (data) => [{ value: "A" }, ...data]);
115
- });
116
-
117
- assert.deepEqual(component.toJSON(), {
118
- type: "div",
119
- props: {},
120
- children: [
121
- {
122
- type: "div",
123
- props: {},
124
- children: ["A"],
125
- },
126
- {
127
- type: "div",
128
- props: {},
129
- children: ["B"],
130
- },
131
- {
132
- type: "div",
133
- props: {},
134
- children: ["C"],
135
- },
136
- ],
137
- });
138
-
139
- assert.equal(divInstances.length, 3);
140
- assert.equal(divInstances[0].store.get("$item.value"), "A");
141
- assert.equal(divInstances[1].store.get("$item.value"), "B");
142
- assert.equal(divInstances[2].store.get("$item.value"), "C");
143
- });
144
-
145
- it("infers T from AccessorChain<T[]> for onCreateFilter callback", () => {
146
- interface Item {
147
- name: string;
148
- active: boolean;
149
- }
150
-
151
- interface AppModel {
152
- items: Item[];
153
- $item: Item;
154
- }
155
-
156
- const m = createAccessorModelProxy<AppModel>();
157
-
158
- // onCreateFilter should receive (record: Item) => boolean when T is inferred from AccessorChain<Item[]>
159
- const widget = (
160
- <cx>
161
- <div>
162
- <Repeater
163
- records={m.items}
164
- recordAlias={m.$item}
165
- onCreateFilter={() => (record) => {
166
- // If T is correctly inferred as Item, record.name should be string
167
- const name: string = record.name;
168
- // @ts-expect-error - record.name should be string, not number
169
- const wrong: number = record.name;
170
- return record.active;
171
- }}
172
- >
173
- <div text={m.$item.name} />
174
- </Repeater>
175
- </div>
176
- </cx>
177
- );
178
-
179
- assert.ok(widget);
180
- });
181
- });
1
+ import { Store } from "../data/Store";
2
+ import { Repeater } from "./Repeater";
3
+ import { bind } from "./bind";
4
+ import { createTestRenderer, act } from "../util/test/createTestRenderer";
5
+ import { createAccessorModelProxy } from "../data/createAccessorModelProxy";
6
+
7
+ import assert from "assert";
8
+
9
+ describe("Repeater", () => {
10
+ it("allows sorting", async () => {
11
+ let data = [
12
+ {
13
+ value: "C",
14
+ },
15
+ {
16
+ value: "B",
17
+ },
18
+ {
19
+ value: "A",
20
+ },
21
+ ];
22
+
23
+ let widget = (
24
+ <cx>
25
+ <div>
26
+ <Repeater records={data} sorters={[{ field: "value", direction: "ASC" }]} recordAlias="$item">
27
+ <div text={bind("$item.value")} />
28
+ </Repeater>
29
+ </div>
30
+ </cx>
31
+ );
32
+
33
+ let store = new Store();
34
+
35
+ const component = await createTestRenderer(store, widget);
36
+
37
+ let tree = component.toJSON();
38
+ assert.deepEqual(tree, {
39
+ type: "div",
40
+ props: {},
41
+ children: [
42
+ {
43
+ type: "div",
44
+ props: {},
45
+ children: ["A"],
46
+ },
47
+ {
48
+ type: "div",
49
+ props: {},
50
+ children: ["B"],
51
+ },
52
+ {
53
+ type: "div",
54
+ props: {},
55
+ children: ["C"],
56
+ },
57
+ ],
58
+ });
59
+ });
60
+
61
+ it("changes are properly updated", async () => {
62
+ let divInstances: any[] = [];
63
+ let widget = (
64
+ <cx>
65
+ <div>
66
+ <Repeater records={bind("data")} sorters={[{ field: "value", direction: "ASC" }]} recordAlias="$item">
67
+ <div
68
+ text={bind("$item.value")}
69
+ onExplore={(context, instance) => {
70
+ divInstances.push(instance);
71
+ }}
72
+ />
73
+ </Repeater>
74
+ </div>
75
+ </cx>
76
+ );
77
+
78
+ let store = new Store({
79
+ data: {
80
+ data: [
81
+ {
82
+ value: "C",
83
+ },
84
+ {
85
+ value: "B",
86
+ },
87
+ ],
88
+ },
89
+ });
90
+
91
+ const component = await createTestRenderer(store, widget);
92
+
93
+ let tree = component.toJSON();
94
+ assert.deepEqual(tree, {
95
+ type: "div",
96
+ props: {},
97
+ children: [
98
+ {
99
+ type: "div",
100
+ props: {},
101
+ children: ["B"],
102
+ },
103
+ {
104
+ type: "div",
105
+ props: {},
106
+ children: ["C"],
107
+ },
108
+ ],
109
+ });
110
+
111
+ divInstances = [];
112
+
113
+ await act(async () => {
114
+ store.update("data", (data) => [{ value: "A" }, ...data]);
115
+ });
116
+
117
+ assert.deepEqual(component.toJSON(), {
118
+ type: "div",
119
+ props: {},
120
+ children: [
121
+ {
122
+ type: "div",
123
+ props: {},
124
+ children: ["A"],
125
+ },
126
+ {
127
+ type: "div",
128
+ props: {},
129
+ children: ["B"],
130
+ },
131
+ {
132
+ type: "div",
133
+ props: {},
134
+ children: ["C"],
135
+ },
136
+ ],
137
+ });
138
+
139
+ assert.equal(divInstances.length, 3);
140
+ assert.equal(divInstances[0].store.get("$item.value"), "A");
141
+ assert.equal(divInstances[1].store.get("$item.value"), "B");
142
+ assert.equal(divInstances[2].store.get("$item.value"), "C");
143
+ });
144
+
145
+ it("infers T from AccessorChain<T[]> for onCreateFilter callback", () => {
146
+ interface Item {
147
+ name: string;
148
+ active: boolean;
149
+ }
150
+
151
+ interface AppModel {
152
+ items: Item[];
153
+ $item: Item;
154
+ }
155
+
156
+ const m = createAccessorModelProxy<AppModel>();
157
+
158
+ // onCreateFilter should receive (record: Item) => boolean when T is inferred from AccessorChain<Item[]>
159
+ const widget = (
160
+ <cx>
161
+ <div>
162
+ <Repeater
163
+ records={m.items}
164
+ recordAlias={m.$item}
165
+ onCreateFilter={() => (record) => {
166
+ // If T is correctly inferred as Item, record.name should be string
167
+ const name: string = record.name;
168
+ // @ts-expect-error - record.name should be string, not number
169
+ const wrong: number = record.name;
170
+ return record.active;
171
+ }}
172
+ >
173
+ <div text={m.$item.name} />
174
+ </Repeater>
175
+ </div>
176
+ </cx>
177
+ );
178
+
179
+ assert.ok(widget);
180
+ });
181
+ });
@@ -47,6 +47,17 @@ describe("Format", function () {
47
47
  });
48
48
  });
49
49
 
50
+ describe("daybefore", function () {
51
+ it("formats the date shifted back by one day", function () {
52
+ assert.equal(Format.value(new Date(2015, 3, 1, 5, 6, 14), "daybefore"), "3/31/2015 05:06");
53
+ assert.equal(Format.value(new Date(2015, 3, 1, 5, 6, 14), "dayBefore"), "3/31/2015 05:06");
54
+ });
55
+
56
+ it("steps back across month and year boundaries", function () {
57
+ assert.equal(Format.value(new Date(2021, 0, 1), "daybefore"), "12/31/2020 00:00");
58
+ });
59
+ });
60
+
50
61
  describe("ellipsis", function () {
51
62
  it("can shorten long texts", function () {
52
63
  assert.equal(Format.value("This is a very long text.", "ellipsis;7"), "This...");
@@ -5,6 +5,7 @@ import { isUndefined } from "../util/isUndefined";
5
5
  import { isArray } from "../util/isArray";
6
6
  import { capitalize } from "./capitalize";
7
7
  import { parseDateInvariant } from "./date/parseDateInvariant";
8
+ import { dayBefore } from "./date/dayBefore";
8
9
 
9
10
  //Culture dependent formatters are defined in the ui package.
10
11
 
@@ -98,6 +99,11 @@ let formatFactory: Record<string, (...args: any[]) => (value: any) => string> =
98
99
  return (value: any) => date(value) + " " + time(value);
99
100
  },
100
101
 
102
+ dayBefore: function () {
103
+ let datetime = formatFactory.datetime();
104
+ return (value: any) => datetime(dayBefore(parseDateInvariant(value)));
105
+ },
106
+
101
107
  ellipsis: function (part0, length, where) {
102
108
  length = Number(length);
103
109
  if (!(length > 3)) length = 10;
@@ -168,6 +174,7 @@ formatFactory.ps = formatFactory.percentageSign;
168
174
  formatFactory.d = formatFactory.date;
169
175
  formatFactory.t = formatFactory.time;
170
176
  formatFactory.dt = formatFactory.datetime;
177
+ formatFactory.daybefore = formatFactory.dayBefore;
171
178
  formatFactory.zeropad = formatFactory.zeroPad;
172
179
  formatFactory.leftpad = formatFactory.leftPad;
173
180
  formatFactory.capitalize = formatFactory.capitalize;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Returns the calendar quarter (1-4) the given date falls in.
3
+ * @param date
4
+ * @returns {number}
5
+ */
6
+ export function dateQuarter(date: Date): number {
7
+ return Math.floor(date.getMonth() / 3) + 1;
8
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Returns a new `Date` representing the calendar day before the given date,
3
+ * keeping the same time of day. Month and year boundaries are handled
4
+ * automatically. The input is not mutated.
5
+ *
6
+ * Useful for displaying the exclusive end of a date range as an inclusive
7
+ * value, e.g. a period ending at `2021-01-01` shown as `Dec 2020`.
8
+ * @param date
9
+ * @returns {Date}
10
+ */
11
+ export function dayBefore(date: Date): Date {
12
+ let result = new Date(date.getTime());
13
+ result.setDate(result.getDate() - 1);
14
+ return result;
15
+ }
@@ -1,4 +1,6 @@
1
1
  export * from "./dateDiff";
2
+ export * from "./dateQuarter";
3
+ export * from "./dayBefore";
2
4
  export * from "./zeroTime";
3
5
  export * from "./monthStart";
4
6
  export * from "./lowerBoundCheck";