-
Notifications
You must be signed in to change notification settings - Fork 190
/
timeMath.js
267 lines (241 loc) · 8.23 KB
/
timeMath.js
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import { Nat } from '@endo/nat';
import { mustMatch } from '@endo/patterns';
import { RelativeTimeRecordShape, TimestampRecordShape } from './typeGuards.js';
/** @import {RelativeTime, RelativeTimeValue, TimerBrand, TimeMathType, Timestamp, TimestampRecord, TimestampValue} from './types.js' */
const { Fail, quote: q } = assert;
/**
* `agreedTimerBrand` is internal to this module.
*
* @param {TimerBrand | undefined} leftBrand
* @param {TimerBrand | undefined} rightBrand
* @returns {TimerBrand | undefined}
*/
const agreedTimerBrand = (leftBrand, rightBrand) => {
if (leftBrand === undefined) {
if (rightBrand === undefined) {
return undefined;
} else {
return rightBrand;
}
} else if (rightBrand === undefined) {
return leftBrand;
} else {
leftBrand === rightBrand ||
Fail`TimerBrands must match: ${q(leftBrand)} vs ${q(rightBrand)}`;
return leftBrand;
}
};
/**
* `sharedTimerBrand` is internal to this module, and implements the
* transitional brand checking and contaigion logic explained in the `TimeMath`
* comment. It is used to define the binary operators that should follow
* this logic. It does the error checking between the operands, and returns
* the brand, if any, that should label the resulting time value.
*
* @param {Timestamp | RelativeTime} left
* @param {Timestamp | RelativeTime} right
* @returns {TimerBrand | undefined}
*/
const sharedTimerBrand = (left, right) => {
const leftBrand = typeof left === 'bigint' ? undefined : left.timerBrand;
const rightBrand = typeof right === 'bigint' ? undefined : right.timerBrand;
return agreedTimerBrand(leftBrand, rightBrand);
};
/**
* `absLike` is internal to this module, and used to implement the binary
* operators in the case where the returned time should be a `Timestamp`
* rather than a `RelativeTime`.
*
* @param {Timestamp | RelativeTime} left
* @param {Timestamp | RelativeTime} right
* @param {TimestampValue} absValue
* @returns {Timestamp}
*/
const absLike = (left, right, absValue) => {
Nat(absValue);
const timerBrand = sharedTimerBrand(left, right);
if (timerBrand) {
return harden({
timerBrand,
absValue,
});
} else {
return absValue;
}
};
/**
* `relLike` is internal to this module, and used to implement the binary
* operators in the case where the returned time should be a `RelativeTime`
* rather than a `Timestamp`.
*
* @param {Timestamp | RelativeTime} left
* @param {Timestamp | RelativeTime} right
* @param {RelativeTimeValue} relValue
* @returns {RelativeTime}
*/
const relLike = (left, right, relValue) => {
Nat(relValue);
const timerBrand = sharedTimerBrand(left, right);
if (timerBrand) {
return harden({
timerBrand,
relValue,
});
} else {
return relValue;
}
};
// For all the following time operators, their documentation is in
// the `TimeMathType`, since that is the documentation that shows up
// in the IDE. Well, at least the vscode IDE.
const absValue = abs => {
if (typeof abs === 'bigint') {
return Nat(abs);
}
mustMatch(abs, TimestampRecordShape, 'timestamp');
return Nat(abs.absValue);
};
const relValue = rel => {
if (typeof rel === 'bigint') {
return Nat(rel);
}
mustMatch(rel, RelativeTimeRecordShape, 'relative');
return Nat(rel.relValue);
};
const makeTimestampRecord = (abs, timerBrand) =>
harden({ absValue: abs, timerBrand });
const makeRelativeTimeRecord = (rel, timerBrand) =>
harden({ relValue: rel, timerBrand });
const coerceTimestampRecord = (ts, brand) => {
brand || Fail`must have a brand`;
if (typeof ts === 'number') {
ts = Nat(ts);
}
if (typeof ts === 'bigint') {
return makeTimestampRecord(ts, brand);
} else {
const { timerBrand } = ts;
mustMatch(ts, TimestampRecordShape, 'timestamp');
agreedTimerBrand(timerBrand, brand);
return ts;
}
};
const coerceRelativeTimeRecord = (rt, brand) => {
brand || Fail`must have a brand`;
if (typeof rt === 'number') {
rt = Nat(rt);
}
if (typeof rt === 'bigint') {
return makeRelativeTimeRecord(rt, brand);
} else {
const { timerBrand } = rt;
mustMatch(rt, RelativeTimeRecordShape, 'relativeTime');
agreedTimerBrand(timerBrand, brand);
return rt;
}
};
const addAbsRel = (abs, rel) =>
absLike(abs, rel, absValue(abs) + relValue(rel));
const addRelRel = (rel1, rel2) =>
relLike(rel1, rel2, relValue(rel1) + relValue(rel2));
const subtractAbsAbs = (abs1, abs2) =>
relLike(abs1, abs2, absValue(abs1) - absValue(abs2));
const clampedSubtractAbsAbs = (abs1, abs2) => {
const val1 = absValue(abs1);
const val2 = absValue(abs2);
return relLike(abs1, abs2, val1 > val2 ? val1 - val2 : 0n);
};
const subtractAbsRel = (abs, rel) =>
absLike(abs, rel, absValue(abs) - relValue(rel));
const subtractRelRel = (rel1, rel2) =>
relLike(rel1, rel2, relValue(rel1) - relValue(rel2));
const isRelZero = rel => relValue(rel) === 0n;
const multiplyRelNat = (rel, nat) => relLike(rel, nat, relValue(rel) * nat);
const divideRelNat = (rel, nat) => relLike(rel, nat, relValue(rel) / nat);
const divideRelRel = (rel1, rel2) => {
sharedTimerBrand(rel1, rel2); // just error check
return relValue(rel1) / relValue(rel2);
};
const modAbsRel = (abs, step) =>
relLike(abs, step, absValue(abs) % relValue(step));
const modRelRel = (rel, step) =>
relLike(rel, step, relValue(rel) % relValue(step));
/**
* `compareValues` is internal to this module, and used to implement
* the time comparison operators.
*
* @param {Timestamp | RelativeTime} left
* @param {Timestamp | RelativeTime} right
* @param {bigint} v1
* @param {bigint} v2
* @returns {import('@endo/marshal').RankComparison}
*/
const compareValues = (left, right, v1, v2) => {
sharedTimerBrand(left, right);
if (v1 < v2) {
return -1;
} else if (v1 === v2) {
return 0;
} else {
assert(v1 > v2);
return 1;
}
};
/**
* The `TimeMath` object provides helper methods to do arithmetic on labeled
* time values, much like `AmountMath` provides helper methods to do arithmetic
* on labeled asset/money values. Both check for consistency of labels: a
* binary operation on two labeled objects ensures that the both carry
* the same label. If they produce another object from the same domain, it
* will carry the same label. If the operands have incompatible labels,
* an error is thrown.
*
* Unlike amount arithmetic, time arithmetic deals in two kinds of time objects:
* Timestamps, which represent absolute time, and RelativeTime, which represents
* the duration between two absolute times. Both kinds of time object
* are labeled by a `TimerBrand`. For a Timestamp object, the value is
* a bigint in an `absValue` property. For a RelativeTime object, the value
* is a bigint in a `relValue` property. Thus we have a runtime safety check
* to ensure that we don't confused the two, even if we have managed to fool
* the (unsound) static type system.
*
* As a transitional measure, currently many Timestamps and RelativeTimes are
* still represented by unlabeled bigints. During this transitional period,
* we allow this, both statically and dynamically. For a normal binary
* operation, if both inputs are labeled, then we do the full checking as
* explained above and return a labeled result. If both inputs are unlabeled
* bigints, we *assume* that they indicate a time of the right kind
* (Timestamp vs RelativeTime) and timer brand. Since we don't know what
* brand was intended, we can only return yet another unlabeled bigint.
*
* If one operand is labeled and the other is not, we check the labeled operand,
* *assume* the unlabeled bigint represents the value needed for the other
* operand, and return a labeled time object with the brand of the labeled
* operand.
*
* @type {TimeMathType}
*/
export const TimeMath = harden({
absValue,
relValue,
coerceTimestampRecord,
coerceRelativeTimeRecord,
// @ts-expect-error xxx dynamic typing
addAbsRel,
// @ts-expect-error xxx dynamic typing
addRelRel,
subtractAbsAbs,
clampedSubtractAbsAbs,
subtractAbsRel,
subtractRelRel,
isRelZero,
multiplyRelNat,
divideRelNat,
divideRelRel,
modAbsRel,
modRelRel,
compareAbs: (abs1, abs2) =>
compareValues(abs1, abs2, absValue(abs1), absValue(abs2)),
compareRel: (rel1, rel2) =>
compareValues(rel1, rel2, relValue(rel1), relValue(rel2)),
});