Skip to content

Commit 920cc53

Browse files
committed
feat(Epoch): apply Hanssen feedback
1 parent cec1fd6 commit 920cc53

File tree

6 files changed

+156
-105
lines changed

6 files changed

+156
-105
lines changed

.changeset/crazy-hairs-greet.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
"@ckb-ccc/core": patch
2+
"@ckb-ccc/core": minor
33
---
44

5-
feat(Epoch): transform `Epoch` into a class and add utility methods
5+
feat(Epoch): transform `Epoch` into a class and add utilities

packages/core/src/ckb/epoch.ts

Lines changed: 137 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -40,86 +40,96 @@ export function epochToHex(epochLike: EpochLike): Hex {
4040

4141
export type EpochLike =
4242
| {
43-
number: NumLike;
44-
index: NumLike;
45-
length: NumLike;
43+
integer: NumLike;
44+
numerator: NumLike;
45+
denominator: NumLike;
4646
}
4747
| [NumLike, NumLike, NumLike];
4848

4949
@mol.codec(
5050
mol
5151
.struct({
52-
length: mol.uint(2, true),
53-
index: mol.uint(2, true),
54-
number: mol.uint(3, true),
52+
padding: mol.Byte8,
53+
value: mol.struct({
54+
denominator: mol.Uint16LE,
55+
numerator: mol.Uint16LE,
56+
integer: mol.uint(3, true),
57+
}),
5558
})
56-
.mapIn((encodable: EpochLike) => Epoch.from(encodable)),
59+
.mapIn((encodable: EpochLike) => {
60+
const value = Epoch.from(encodable);
61+
return {
62+
padding: "0x0000000000000000",
63+
value,
64+
};
65+
})
66+
.mapOut((v) => v.value as Epoch),
5767
)
5868
/**
5969
* Epoch
6070
*
61-
* Represents a timestamp-like epoch as a mixed whole number and fractional part:
62-
* - number: whole units
63-
* - index: numerator of the fractional part
64-
* - length: denominator of the fractional part (must be > 0)
71+
* Represents a timestamp-like epoch as a mixed whole integer and fractional part:
72+
* - integer: whole units
73+
* - numerator: numerator of the fractional part
74+
* - denominator: denominator of the fractional part (must be > 0)
6575
*
66-
* The fractional portion is index/length. Instances normalize fractions where
76+
* The fractional portion is numerator/denominator. Instances normalize fractions where
6777
* appropriate (e.g., reduce by GCD, carry whole units).
6878
*/
6979
export class Epoch extends mol.Entity.Base<EpochLike, Epoch>() {
7080
/**
7181
* Construct a new Epoch.
7282
*
73-
* The constructor enforces a positive `length` (denominator). If `length`
83+
* The constructor enforces a positive `denominator`. If `denominator`
7484
* is non-positive an Error is thrown.
7585
*
76-
* @param number - Whole number portion of the epoch.
77-
* @param index - Fractional numerator.
78-
* @param length - Fractional denominator (must be > 0).
86+
* @param integer - Whole number portion of the epoch.
87+
* @param numerator - Fractional numerator.
88+
* @param denominator - Fractional denominator (must be > 0).
7989
*/
8090
public constructor(
81-
public readonly number: Num,
82-
public readonly index: Num,
83-
public readonly length: Num,
91+
public readonly integer: Num,
92+
public readonly numerator: Num,
93+
public readonly denominator: Num,
8494
) {
8595
// Ensure the epoch has a positive denominator.
86-
if (length <= Zero) {
87-
throw new Error("Non positive Epoch length");
96+
if (denominator <= Zero) {
97+
throw new Error("Non positive Epoch denominator");
8898
}
8999
super();
90100
}
91101

92102
/**
93-
* @deprecated use `number` instead
94-
* Backwards-compatible array-style index 0 referencing the whole epoch number.
103+
* @deprecated use `integer` instead
104+
* Backwards-compatible array-style index 0 referencing the whole epoch integer.
95105
*/
96106
get 0(): Num {
97-
return this.number;
107+
return this.integer;
98108
}
99109

100110
/**
101-
* @deprecated use `index` instead
111+
* @deprecated use `numerator` instead
102112
* Backwards-compatible array-style index 1 referencing the epoch fractional numerator.
103113
*/
104114
get 1(): Num {
105-
return this.index;
115+
return this.numerator;
106116
}
107117

108118
/**
109-
* @deprecated use `length` instead
119+
* @deprecated use `denominator` instead
110120
* Backwards-compatible array-style index 2 referencing the epoch fractional denominator.
111121
*/
112122
get 2(): Num {
113-
return this.length;
123+
return this.denominator;
114124
}
115125

116126
/**
117127
* Create an Epoch from an EpochLike value.
118128
*
119129
* Accepts:
120130
* - an Epoch instance (returned as-is)
121-
* - an object { number, index, length } where each field is NumLike
122-
* - a tuple [number, index, length] where each element is NumLike
131+
* - an object { integer, numerator, denominator } where each field is NumLike
132+
* - a tuple [integer, numerator, denominator] where each element is NumLike
123133
*
124134
* All returned fields are converted to `Num` using `numFrom`.
125135
*
@@ -131,14 +141,18 @@ export class Epoch extends mol.Entity.Base<EpochLike, Epoch>() {
131141
return epochLike;
132142
}
133143

134-
let number: NumLike, index: NumLike, length: NumLike;
135-
if (epochLike instanceof Array) {
136-
[number, index, length] = epochLike;
144+
let integer: NumLike, numerator: NumLike, denominator: NumLike;
145+
if (Array.isArray(epochLike)) {
146+
[integer, numerator, denominator] = epochLike;
137147
} else {
138-
({ number, index, length } = epochLike);
148+
({ integer, numerator, denominator } = epochLike);
139149
}
140150

141-
return new Epoch(numFrom(number), numFrom(index), numFrom(length));
151+
return new Epoch(
152+
numFrom(integer),
153+
numFrom(numerator),
154+
numFrom(denominator),
155+
);
142156
}
143157

144158
/**
@@ -148,27 +162,20 @@ export class Epoch extends mol.Entity.Base<EpochLike, Epoch>() {
148162
return new Epoch(0n, 0n, numFrom(1));
149163
}
150164

151-
/**
152-
* Return an epoch representing one (1 + 0/1).
153-
*/
154-
static one(): Epoch {
155-
return new Epoch(numFrom(1), 0n, numFrom(1));
156-
}
157-
158165
/**
159166
* Return an epoch representing one cycle (180 + 0/1).
160167
*
161168
* This is a NervosDAO convenience constant.
162169
*/
163-
static oneCycle(): Epoch {
170+
static oneNervosDaoCycle(): Epoch {
164171
return new Epoch(numFrom(180), 0n, numFrom(1));
165172
}
166173

167174
/**
168175
* Compare this epoch to another EpochLike.
169176
*
170177
* Comparison is performed by converting both epochs to a common integer
171-
* representation: (number * length + index) scaled by the other's length.
178+
* representation: (integer * denominator + numerator) scaled by the other's denominator.
172179
*
173180
* @param other - EpochLike value to compare against.
174181
* @returns 1 if this > other, 0 if equal, -1 if this < other.
@@ -178,54 +185,95 @@ export class Epoch extends mol.Entity.Base<EpochLike, Epoch>() {
178185
return 0;
179186
}
180187

181-
const other_ = Epoch.from(other);
182-
const a = (this.number * this.length + this.index) * other_.length;
183-
const b = (other_.number * other_.length + other_.index) * this.length;
188+
const o = Epoch.from(other);
189+
const a =
190+
(this.integer * this.denominator + this.numerator) * o.denominator;
191+
const b = (o.integer * o.denominator + o.numerator) * this.denominator;
184192

185193
return a > b ? 1 : a < b ? -1 : 0;
186194
}
187195

188196
/**
189-
* Check equality with another EpochLike.
197+
* Check whether this epoch is less than another EpochLike.
198+
*
199+
* @param other - EpochLike to compare against.
200+
* @returns true if this epoch is strictly less than the other.
201+
*/
202+
lt(other: EpochLike): boolean {
203+
return this.compare(other) < 0;
204+
}
205+
206+
/**
207+
* Check whether this epoch is less than or equal to another EpochLike.
208+
*
209+
* @param other - EpochLike to compare against.
210+
* @returns true if this epoch is less than or equal to the other.
211+
*/
212+
le(other: EpochLike): boolean {
213+
return this.compare(other) <= 0;
214+
}
215+
216+
/**
217+
* Check whether this epoch is equal to another EpochLike.
190218
*
191-
* @param other - EpochLike to test equality against.
219+
* @param other - EpochLike to compare against.
192220
* @returns true if both epochs represent the same value.
193221
*/
194222
eq(other: EpochLike): boolean {
195223
return this.compare(other) === 0;
196224
}
197225

226+
/**
227+
* Check whether this epoch is greater than or equal to another EpochLike.
228+
*
229+
* @param other - EpochLike to compare against.
230+
* @returns true if this epoch is greater than or equal to the other.
231+
*/
232+
ge(other: EpochLike): boolean {
233+
return this.compare(other) >= 0;
234+
}
235+
236+
/**
237+
* Check whether this epoch is greater than another EpochLike.
238+
*
239+
* @param other - EpochLike to compare against.
240+
* @returns true if this epoch is strictly greater than the other.
241+
*/
242+
gt(other: EpochLike): boolean {
243+
return this.compare(other) > 0;
244+
}
245+
198246
/**
199247
* Return a normalized epoch:
200-
* - Ensures index is non-negative by borrowing from `number` if needed.
201-
* - Reduces the fraction (index/length) by their GCD.
202-
* - Carries any whole units from the fraction into `number`.
248+
* - Ensures numerator is non-negative by borrowing from `integer` if needed.
249+
* - Reduces the fraction (numerator/denominator) by their GCD.
250+
* - Carries any whole units from the fraction into `integer`.
203251
*
204252
* @returns A new, normalized Epoch instance.
205253
*/
206254
normalized(): Epoch {
207-
let { number, index, length } = this;
255+
let { integer, numerator, denominator } = this;
208256

209-
// Normalize negative index values by borrowing from the whole number.
210-
if (index < Zero) {
257+
// Normalize negative numerator values by borrowing from the whole integer.
258+
if (numerator < Zero) {
211259
// Calculate how many whole units to borrow.
212-
const n = (-index + length - 1n) / length;
213-
number -= n;
214-
index += length * n;
260+
const n = (-numerator + denominator - 1n) / denominator;
261+
integer -= n;
262+
numerator += denominator * n;
215263
}
216264

217-
// Reduce the fraction (index / length) to its simplest form using the greatest common divisor.
218-
const g = gcd(index, length);
219-
index /= g;
220-
length /= g;
265+
// Reduce the fraction (numerator / denominator) to its simplest form using the greatest common divisor.
266+
const g = gcd(numerator, denominator);
267+
numerator /= g;
268+
denominator /= g;
221269

222-
// Add any whole number overflow from the fraction.
223-
number += index / length;
270+
// Add any whole integer overflow from the fraction.
271+
integer += numerator / denominator;
224272

225-
// Calculate the leftover index after accounting for the whole number part from the fraction.
226-
index %= length;
273+
// Calculate the leftover numerator after accounting for the whole integer part from the fraction.
274+
numerator %= denominator;
227275

228-
return new Epoch(number, index, length);
276+
return new Epoch(integer, numerator, denominator);
229277
}
230278

231279
/**
@@ -238,24 +286,25 @@ export class Epoch extends mol.Entity.Base<EpochLike, Epoch>() {
238286
* @returns New Epoch equal to this + other.
239287
*/
240288
add(other: EpochLike): Epoch {
241-
const other_ = Epoch.from(other);
242-
243-
// Sum the whole number parts.
244-
const number = this.number + other_.number;
245-
let index: Num;
246-
let length: Num;
247-
248-
// If the epochs have different denominators (lengths), align them to a common denominator.
249-
if (this.length !== other_.length) {
250-
index = other_.index * this.length + this.index * other_.length;
251-
length = this.length * other_.length;
289+
const o = Epoch.from(other);
290+
291+
// Sum the whole integer parts.
292+
const integer = this.integer + o.integer;
293+
let numerator: Num;
294+
let denominator: Num;
295+
296+
// If the epochs have different denominators, align them to a common denominator.
297+
if (this.denominator !== o.denominator) {
298+
numerator =
299+
o.numerator * this.denominator + this.numerator * o.denominator;
300+
denominator = this.denominator * o.denominator;
252301
} else {
253-
// If denominators are equal, simply add the indices.
254-
index = this.index + other_.index;
255-
length = this.length;
302+
// If denominators are equal, simply add the numerators.
303+
numerator = this.numerator + o.numerator;
304+
denominator = this.denominator;
256305
}
257306

258-
return new Epoch(number, index, length).normalized();
307+
return new Epoch(integer, numerator, denominator).normalized();
259308
}
260309

261310
/**
@@ -265,8 +314,8 @@ export class Epoch extends mol.Entity.Base<EpochLike, Epoch>() {
265314
* @returns New Epoch equal to this - other.
266315
*/
267316
sub(other: EpochLike): Epoch {
268-
const { number, index, length } = Epoch.from(other);
269-
return this.add(new Epoch(-number, -index, length));
317+
const { integer, numerator, denominator } = Epoch.from(other);
318+
return this.add(new Epoch(-integer, -numerator, denominator));
270319
}
271320

272321
/**
@@ -277,12 +326,12 @@ export class Epoch extends mol.Entity.Base<EpochLike, Epoch>() {
277326
*/
278327
toUnix(reference: ClientBlockHeader): bigint {
279328
// Calculate the difference between the provided epoch and the reference epoch.
280-
const { number, index, length } = this.sub(reference.epoch);
329+
const { integer, numerator, denominator } = this.sub(reference.epoch);
281330

282331
return (
283332
reference.timestamp +
284-
epochInMilliseconds * number +
285-
(epochInMilliseconds * index) / length
333+
EPOCH_IN_MILLISECONDS * integer +
334+
(EPOCH_IN_MILLISECONDS * numerator) / denominator
286335
);
287336
}
288337
}
@@ -293,4 +342,4 @@ export class Epoch extends mol.Entity.Base<EpochLike, Epoch>() {
293342
* Calculated as 4 hours in milliseconds:
294343
* 4 hours * 60 minutes per hour * 60 seconds per minute * 1000 milliseconds per second.
295344
*/
296-
const epochInMilliseconds = numFrom(14400000); // (Number.isSafeInteger(14400000) === true)
345+
const EPOCH_IN_MILLISECONDS = numFrom(4 * 60 * 60 * 1000);

0 commit comments

Comments
 (0)