|
| 1 | +import type { ClientBlockHeader } from "../client/clientTypes.js"; |
| 2 | +import { Zero } from "../fixedPoint/index.js"; |
| 3 | +import { type Hex, type HexLike } from "../hex/index.js"; |
| 4 | +import { mol } from "../molecule/index.js"; |
| 5 | +import { numFrom, NumLike, type Num } from "../num/index.js"; |
| 6 | + |
| 7 | +/** |
| 8 | + * @deprecated use `Epoch.from` instead |
| 9 | + * Convert an Epoch-like value into an Epoch instance. |
| 10 | + * |
| 11 | + * @param epochLike - An EpochLike value (object or tuple). |
| 12 | + * @returns An Epoch instance built from `epochLike`. |
| 13 | + */ |
| 14 | +export function epochFrom(epochLike: EpochLike): Epoch { |
| 15 | + return Epoch.from(epochLike); |
| 16 | +} |
| 17 | + |
| 18 | +/** |
| 19 | + * @deprecated use `Epoch.decode` instead |
| 20 | + * Decode an epoch from a hex-like representation. |
| 21 | + * |
| 22 | + * @param hex - A hex-like value representing an encoded epoch. |
| 23 | + * @returns An Epoch instance decoded from `hex`. |
| 24 | + */ |
| 25 | +export function epochFromHex(hex: HexLike): Epoch { |
| 26 | + return Epoch.decode(hex); |
| 27 | +} |
| 28 | + |
| 29 | +/** |
| 30 | + * @deprecated use `Epoch.from(epochLike).toHex` instead |
| 31 | + * Convert an Epoch-like value to its hex representation. |
| 32 | + * |
| 33 | + * @param epochLike - An EpochLike value (object, tuple, or Epoch). |
| 34 | + * @returns Hex string representing the epoch. |
| 35 | + */ |
| 36 | +export function epochToHex(epochLike: EpochLike): Hex { |
| 37 | + return Epoch.from(epochLike).toHex(); |
| 38 | +} |
| 39 | + |
| 40 | +export type EpochLike = |
| 41 | + | { |
| 42 | + number: NumLike; |
| 43 | + index: NumLike; |
| 44 | + length: NumLike; |
| 45 | + } |
| 46 | + | [NumLike, NumLike, NumLike]; |
| 47 | + |
| 48 | +@mol.codec( |
| 49 | + mol |
| 50 | + .struct({ |
| 51 | + length: mol.uint(2, true), |
| 52 | + index: mol.uint(2, true), |
| 53 | + number: mol.uint(3, true), |
| 54 | + }) |
| 55 | + .mapIn((encodable: EpochLike) => Epoch.from(encodable)), |
| 56 | +) |
| 57 | +/** |
| 58 | + * Epoch |
| 59 | + * |
| 60 | + * Represents a timestamp-like epoch as a mixed whole number and fractional part: |
| 61 | + * - number: whole units |
| 62 | + * - index: numerator of the fractional part |
| 63 | + * - length: denominator of the fractional part (must be > 0) |
| 64 | + * |
| 65 | + * The fractional portion is index/length. Instances normalize fractions where |
| 66 | + * appropriate (e.g., reduce by GCD, carry whole units). |
| 67 | + */ |
| 68 | +export class Epoch extends mol.Entity.Base<EpochLike, Epoch>() { |
| 69 | + /** |
| 70 | + * @deprecated use `number` instead |
| 71 | + * Backwards-compatible array-style index 0 referencing the whole number. |
| 72 | + */ |
| 73 | + public readonly [0]: Num; |
| 74 | + /** |
| 75 | + * @deprecated use `index` instead |
| 76 | + * Backwards-compatible array-style index 1 referencing the fractional numerator. |
| 77 | + */ |
| 78 | + public readonly [1]: Num; |
| 79 | + /** |
| 80 | + * @deprecated use `length` instead |
| 81 | + * Backwards-compatible array-style index 2 referencing the fractional denominator. |
| 82 | + */ |
| 83 | + public readonly [2]: Num; |
| 84 | + |
| 85 | + /** |
| 86 | + * Construct a new Epoch. |
| 87 | + * |
| 88 | + * The constructor enforces a positive `length` (denominator). If `length` |
| 89 | + * is non-positive an Error is thrown. |
| 90 | + * |
| 91 | + * @param number - Whole number portion of the epoch. |
| 92 | + * @param index - Fractional numerator. |
| 93 | + * @param length - Fractional denominator (must be > 0). |
| 94 | + */ |
| 95 | + public constructor( |
| 96 | + public readonly number: Num, |
| 97 | + public readonly index: Num, |
| 98 | + public readonly length: Num, |
| 99 | + ) { |
| 100 | + // Ensure the epoch has a positive denominator. |
| 101 | + if (length <= Zero) { |
| 102 | + throw new Error("Non positive Epoch length"); |
| 103 | + } |
| 104 | + super(); |
| 105 | + this[0] = number; |
| 106 | + this[1] = index; |
| 107 | + this[2] = length; |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * Create an Epoch from an EpochLike value. |
| 112 | + * |
| 113 | + * Accepts: |
| 114 | + * - an Epoch instance (returned as-is) |
| 115 | + * - an object { number, index, length } where each field is NumLike |
| 116 | + * - a tuple [number, index, length] where each element is NumLike |
| 117 | + * |
| 118 | + * All returned fields are converted to `Num` using `numFrom`. |
| 119 | + * |
| 120 | + * @param epochLike - Value to convert into an Epoch. |
| 121 | + * @returns A new or existing Epoch instance. |
| 122 | + */ |
| 123 | + static from(epochLike: EpochLike): Epoch { |
| 124 | + if (epochLike instanceof Epoch) { |
| 125 | + return epochLike; |
| 126 | + } |
| 127 | + |
| 128 | + let number: NumLike, index: NumLike, length: NumLike; |
| 129 | + if (epochLike instanceof Array) { |
| 130 | + [number, index, length] = epochLike; |
| 131 | + } else { |
| 132 | + ({ number, index, length } = epochLike); |
| 133 | + } |
| 134 | + |
| 135 | + return new Epoch(numFrom(number), numFrom(index), numFrom(length)); |
| 136 | + } |
| 137 | + |
| 138 | + /** |
| 139 | + * Return an epoch representing zero (0 + 0/1). |
| 140 | + */ |
| 141 | + static zero(): Epoch { |
| 142 | + return new Epoch(0n, 0n, numFrom(1)); |
| 143 | + } |
| 144 | + |
| 145 | + /** |
| 146 | + * Return an epoch representing one (1 + 0/1). |
| 147 | + */ |
| 148 | + static one(): Epoch { |
| 149 | + return new Epoch(numFrom(1), 0n, numFrom(1)); |
| 150 | + } |
| 151 | + |
| 152 | + /** |
| 153 | + * Return an epoch representing one cycle (180 + 0/1). |
| 154 | + * |
| 155 | + * This is a NervosDAO convenience constant. |
| 156 | + */ |
| 157 | + static oneCycle(): Epoch { |
| 158 | + return new Epoch(numFrom(180), 0n, numFrom(1)); |
| 159 | + } |
| 160 | + |
| 161 | + /** |
| 162 | + * Compare this epoch to another EpochLike. |
| 163 | + * |
| 164 | + * Comparison is performed by converting both epochs to a common integer |
| 165 | + * representation: (number * length + index) scaled by the other's length. |
| 166 | + * |
| 167 | + * @param other - EpochLike value to compare against. |
| 168 | + * @returns positive if this > other, 0 if equal, negative if this < other. |
| 169 | + */ |
| 170 | + compare(other: EpochLike): number { |
| 171 | + if (this === other) { |
| 172 | + return 0; |
| 173 | + } |
| 174 | + |
| 175 | + const other_ = Epoch.from(other); |
| 176 | + const a = (this.number * this.length + this.index) * other_.length; |
| 177 | + const b = (other_.number * other_.length + other_.index) * this.length; |
| 178 | + |
| 179 | + return Number(a - b); |
| 180 | + } |
| 181 | + |
| 182 | + /** |
| 183 | + * Check equality with another EpochLike. |
| 184 | + * |
| 185 | + * @param other - EpochLike to test equality against. |
| 186 | + * @returns true if both epochs represent the same value. |
| 187 | + */ |
| 188 | + eq(other: EpochLike): boolean { |
| 189 | + return this.compare(other) === 0; |
| 190 | + } |
| 191 | + |
| 192 | + /** |
| 193 | + * Return a normalized epoch: |
| 194 | + * - Ensures index is non-negative by borrowing from `number` if needed. |
| 195 | + * - Reduces the fraction (index/length) by their GCD. |
| 196 | + * - Carries any whole units from the fraction into `number`. |
| 197 | + * |
| 198 | + * @returns A new, normalized Epoch instance. |
| 199 | + */ |
| 200 | + normalized(): Epoch { |
| 201 | + let { number, index, length } = this; |
| 202 | + |
| 203 | + // Normalize negative index values by borrowing from the whole number. |
| 204 | + if (index < Zero) { |
| 205 | + // Calculate how many whole units to borrow. |
| 206 | + const n = (-index + length - 1n) / length; |
| 207 | + number -= n; |
| 208 | + index += length * n; |
| 209 | + } |
| 210 | + |
| 211 | + // Reduce the fraction (index / length) to its simplest form using the greatest common divisor. |
| 212 | + const g = gcd(index, length); |
| 213 | + index /= g; |
| 214 | + length /= g; |
| 215 | + |
| 216 | + // Add any whole number overflow from the fraction. |
| 217 | + number += index / length; |
| 218 | + |
| 219 | + // Calculate the leftover index after accounting for the whole number part from the fraction. |
| 220 | + index %= length; |
| 221 | + |
| 222 | + return new Epoch(number, index, length); |
| 223 | + } |
| 224 | + |
| 225 | + /** |
| 226 | + * Add another epoch to this one. |
| 227 | + * |
| 228 | + * If denominators differ, the method aligns to a common denominator before |
| 229 | + * adding the fractional numerators, then returns a normalized Epoch. |
| 230 | + * |
| 231 | + * @param other - EpochLike to add. |
| 232 | + * @returns New Epoch equal to this + other. |
| 233 | + */ |
| 234 | + add(other: EpochLike): Epoch { |
| 235 | + const other_ = Epoch.from(other); |
| 236 | + |
| 237 | + // Sum the whole number parts. |
| 238 | + const number = this.number + other_.number; |
| 239 | + let index: Num; |
| 240 | + let length: Num; |
| 241 | + |
| 242 | + // If the epochs have different denominators (lengths), align them to a common denominator. |
| 243 | + if (this.length !== other_.length) { |
| 244 | + index = other_.index * this.length + this.index * other_.length; |
| 245 | + length = this.length * other_.length; |
| 246 | + } else { |
| 247 | + // If denominators are equal, simply add the indices. |
| 248 | + index = this.index + other_.index; |
| 249 | + length = this.length; |
| 250 | + } |
| 251 | + |
| 252 | + return new Epoch(number, index, length).normalized(); |
| 253 | + } |
| 254 | + |
| 255 | + /** |
| 256 | + * Subtract an epoch from this epoch. |
| 257 | + * |
| 258 | + * @param other - EpochLike to subtract. |
| 259 | + * @returns New Epoch equal to this - other. |
| 260 | + */ |
| 261 | + sub(other: EpochLike): Epoch { |
| 262 | + const { number, index, length } = Epoch.from(other); |
| 263 | + return this.add(new Epoch(-number, -index, length)); |
| 264 | + } |
| 265 | + |
| 266 | + /** |
| 267 | + * Convert this epoch to an estimated Unix timestamp in milliseconds using as reference the block header. |
| 268 | + * |
| 269 | + * @param reference - ClientBlockHeader providing a reference epoch and timestamp. |
| 270 | + * @returns Unix timestamp in milliseconds as bigint. |
| 271 | + */ |
| 272 | + toUnix(reference: ClientBlockHeader): bigint { |
| 273 | + // Calculate the difference between the provided epoch and the reference epoch. |
| 274 | + const { number, index, length } = this.sub(reference.epoch); |
| 275 | + |
| 276 | + return ( |
| 277 | + reference.timestamp + |
| 278 | + epochInMilliseconds * number + |
| 279 | + (epochInMilliseconds * index) / length |
| 280 | + ); |
| 281 | + } |
| 282 | +} |
| 283 | + |
| 284 | +/** |
| 285 | + * A constant representing the epoch duration in milliseconds. |
| 286 | + * |
| 287 | + * Calculated as 4 hours in milliseconds: |
| 288 | + * 4 hours * 60 minutes per hour * 60 seconds per minute * 1000 milliseconds per second. |
| 289 | + */ |
| 290 | +const epochInMilliseconds = numFrom(14400000); // (Number.isSafeInteger(14400000) === true) |
| 291 | + |
| 292 | +/** |
| 293 | + * Calculate the greatest common divisor (GCD) of two Num values using the Euclidean algorithm. |
| 294 | + * |
| 295 | + * @param a - First operand. |
| 296 | + * @param b - Second operand. |
| 297 | + * @returns GCD(a, b) as a Num. |
| 298 | + */ |
| 299 | +function gcd(a: Num, b: Num): Num { |
| 300 | + while (b !== Zero) { |
| 301 | + [a, b] = [b, a % b]; |
| 302 | + } |
| 303 | + return a; |
| 304 | +} |
0 commit comments