range-pie 2.4.1 → 3.0.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.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 SkorpionG2000
3
+ Copyright (c) 2026 SkorpionG2000
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -155,13 +155,14 @@ console.log(range.length); // 5
155
155
 
156
156
  ### at()
157
157
 
158
- The 'at' method accepts a number as argument to gets the value at the specified index in a range. Generate a RangeError if the index is out of range.
158
+ The 'at' method accepts a number as argument to get the value at the specified index in a range. Returns `undefined` if the index is out of range, perfectly matching `Array.prototype.at`. Negative indices count backwards from the end of the range.
159
159
 
160
160
  ```javascript
161
161
  const range = new PyRange(1, 5); // [1, 2, 3, 4]
162
162
  console.log(range.at(0)); // 1
163
163
  console.log(range.at(2)); // 3
164
- console.log(range.at(-1)); // RangeError
164
+ console.log(range.at(-1)); // 4 (negative index wraps from the end)
165
+ console.log(range.at(10)); // undefined (out of bounds)
165
166
  ```
166
167
 
167
168
  ### toString()
@@ -400,7 +401,7 @@ const array = [...range]; // [0, 1, 2]
400
401
 
401
402
  ### Proxy Access
402
403
 
403
- Proxy access allows access to the array elements using bracket notation, just like a regular array.
404
+ Proxy access allows access to the array elements using bracket notation, just like a regular array. Out-of-bounds access returns `undefined`.
404
405
 
405
406
  ```javascript
406
407
  const range = new PyRange(5);
@@ -408,6 +409,7 @@ const proxy = range.asProxy();
408
409
 
409
410
  console.log(proxy[0]); // 0
410
411
  console.log(proxy[3]); // 3
412
+ console.log(proxy[10]); // undefined
411
413
  ```
412
414
 
413
415
  ### TypeScript Support
@@ -6,6 +6,8 @@ declare class PyRange implements Iterable<number> {
6
6
  private _stop;
7
7
  private _step;
8
8
  private _length;
9
+ private static toIntegerOrInfinity;
10
+ private static isArrayIndexProperty;
9
11
  /**
10
12
  * Creates a new PyRange instance.
11
13
  * @param {...number} args - The arguments of the range. The possible forms are
@@ -21,7 +23,9 @@ declare class PyRange implements Iterable<number> {
21
23
  * @property {number} step - The step of the range.
22
24
  * @property {number} length - The length of the range.
23
25
  */
24
- constructor(...args: number[]);
26
+ constructor(stop: number);
27
+ constructor(start: number, stop: number);
28
+ constructor(start: number, stop: number, step: number);
25
29
  /**
26
30
  * Gets the length of this range.
27
31
  *
@@ -52,11 +56,17 @@ declare class PyRange implements Iterable<number> {
52
56
  get step(): number;
53
57
  /**
54
58
  * Gets the value at the specified index in this range.
55
- * @param {number} index - The index of the value to retrieve
56
- * @returns {number} The value at the specified index
57
- * @throws {RangeError} If the index is out of range
59
+ *
60
+ * Negative indices are supported and wrap from the end, matching the
61
+ * behaviour of `Array.prototype.at()`. For example, `at(-1)` returns the
62
+ * last element.
63
+ *
64
+ * @param {number} index - The index of the value to retrieve. Negative
65
+ * indices count from the end of the range.
66
+ * @returns {number|undefined} The value at the specified index, or
67
+ * undefined when the normalised index is out of range.
58
68
  */
59
- at(index: number): number;
69
+ at(index: number): number | undefined;
60
70
  /**
61
71
  * Converts the range to a string.
62
72
  * @returns {string} A string of the form `Range(start, stop, step)`.
@@ -71,6 +81,7 @@ declare class PyRange implements Iterable<number> {
71
81
  * Validates that the callback is a function.
72
82
  * @param {Function} cb - The callback to validate.
73
83
  * @throws {TypeError} If the callback is not a function.
84
+ * @internal
74
85
  */
75
86
  private static validateCb;
76
87
  /**
@@ -92,15 +103,23 @@ declare class PyRange implements Iterable<number> {
92
103
  filter(callback: (value: number, index: number, range: PyRange) => boolean): number[];
93
104
  /**
94
105
  * Reduces the range to a single value.
106
+ *
107
+ * Uses rest parameters to reliably distinguish between "no initial value
108
+ * provided" and "explicit `undefined` passed as initial value", matching
109
+ * the behaviour of `Array.prototype.reduce()`.
110
+ *
95
111
  * @param {function(T, number, number, PyRange): T} callback - The callback
96
112
  * function to apply to every element. The callback should take four
97
113
  * arguments: the accumulator, the current value, the index of the current
98
114
  * value, and the range object.
99
115
  * @param {T} [initialValue] - The initial value of the accumulator.
100
116
  * @returns {T} The final value of the accumulator.
117
+ * @throws {TypeError} If the range is empty and no initial value is provided.
101
118
  * @template T
102
119
  */
103
- reduce<T>(callback: (accumulator: T, value: number, index: number, range: PyRange) => T, initialValue?: T): T;
120
+ reduce(callback: (accumulator: number, value: number, index: number, range: PyRange) => number): number;
121
+ reduce(callback: (accumulator: number, value: number, index: number, range: PyRange) => number, initialValue: number): number;
122
+ reduce<U>(callback: (accumulator: U, value: number, index: number, range: PyRange) => U, initialValue: U): U;
104
123
  /**
105
124
  * Determines whether at least one element of the range satisfies the
106
125
  * provided test.
@@ -189,6 +208,10 @@ declare class PyRange implements Iterable<number> {
189
208
  slice(begin?: number, end?: number): PyRange;
190
209
  /**
191
210
  * Reverses the order of the elements in this range, returning a new PyRange object.
211
+ *
212
+ * The new range starts at the last actual element of this range and steps
213
+ * in the opposite direction. The original range is not modified.
214
+ *
192
215
  * @returns {PyRange} A new PyRange object with the elements in reverse order.
193
216
  */
194
217
  reverse(): PyRange;
@@ -219,18 +242,18 @@ declare class PyRange implements Iterable<number> {
219
242
  values(): IterableIterator<number>;
220
243
  /**
221
244
  * Implements the iterable protocol for this range.
222
- * @returns {Iterator<number>} An iterator for this range.
245
+ * @returns {IterableIterator<number>} An iterator for this range.
223
246
  */
224
- [Symbol.iterator](): Iterator<number>;
247
+ [Symbol.iterator](): IterableIterator<number>;
225
248
  /**
226
249
  * Returns a Proxy for this range, allowing indexed access.
227
250
  * This proxy enables accessing range elements via array-like indexing.
228
251
  * If the property is a number, it will return the element at that index.
229
252
  *
230
- * @returns {PyRange & { [key: number]: number }} A proxy for the PyRange instance.
253
+ * @returns {PyRange & { [key: number]: number | undefined }} A proxy for the PyRange instance.
231
254
  */
232
255
  asProxy(): PyRange & {
233
- [key: number]: number;
256
+ [key: number]: number | undefined;
234
257
  };
235
258
  }
236
259
  export { PyRange };
package/dist/py-range.js CHANGED
@@ -5,21 +5,22 @@ exports.PyRange = void 0;
5
5
  * A class that simulates Python's range function, combined with several useful JavaScript array methods.
6
6
  */
7
7
  class PyRange {
8
- /**
9
- * Creates a new PyRange instance.
10
- * @param {...number} args - The arguments of the range. The possible forms are
11
- * - `PyRange(stop)`
12
- * - `PyRange(start, stop)`
13
- * - `PyRange(start, stop, step)`
14
- * @throws {TypeError} If any of the arguments is not a number.
15
- * @throws {TypeError} If any of the arguments is not an integer.
16
- * @throws {Error} If the step is zero.
17
- * @throws {Error} If the arguments count is not between 1 and 3.
18
- * @property {number} start - The start of the range, inclusive.
19
- * @property {number} stop - The stop of the range, exclusive.
20
- * @property {number} step - The step of the range.
21
- * @property {number} length - The length of the range.
22
- */
8
+ _start;
9
+ _stop;
10
+ _step;
11
+ _length;
12
+ static toIntegerOrInfinity(value) {
13
+ const numeric = +value;
14
+ return Number.isNaN(numeric) ? 0 : Math.trunc(numeric);
15
+ }
16
+ static isArrayIndexProperty(prop) {
17
+ const index = Number(prop);
18
+ return (prop !== "" &&
19
+ Number.isInteger(index) &&
20
+ index >= 0 &&
21
+ index < 2 ** 32 - 1 &&
22
+ String(index) === prop);
23
+ }
23
24
  constructor(...args) {
24
25
  if (!args.every((arg) => typeof arg === "number")) {
25
26
  throw new TypeError("All arguments must be numbers");
@@ -83,15 +84,23 @@ class PyRange {
83
84
  }
84
85
  /**
85
86
  * Gets the value at the specified index in this range.
86
- * @param {number} index - The index of the value to retrieve
87
- * @returns {number} The value at the specified index
88
- * @throws {RangeError} If the index is out of range
87
+ *
88
+ * Negative indices are supported and wrap from the end, matching the
89
+ * behaviour of `Array.prototype.at()`. For example, `at(-1)` returns the
90
+ * last element.
91
+ *
92
+ * @param {number} index - The index of the value to retrieve. Negative
93
+ * indices count from the end of the range.
94
+ * @returns {number|undefined} The value at the specified index, or
95
+ * undefined when the normalised index is out of range.
89
96
  */
90
97
  at(index) {
91
- if (index < 0 || index >= this._length) {
92
- throw new RangeError("Index out of range");
98
+ const relativeIndex = PyRange.toIntegerOrInfinity(index);
99
+ const normalised = relativeIndex < 0 ? this._length + relativeIndex : relativeIndex;
100
+ if (normalised < 0 || normalised >= this._length) {
101
+ return undefined;
93
102
  }
94
- return this._start + index * this._step;
103
+ return this._start + normalised * this._step;
95
104
  }
96
105
  /**
97
106
  * Converts the range to a string.
@@ -111,6 +120,7 @@ class PyRange {
111
120
  * Validates that the callback is a function.
112
121
  * @param {Function} cb - The callback to validate.
113
122
  * @throws {TypeError} If the callback is not a function.
123
+ * @internal
114
124
  */
115
125
  static validateCb(cb) {
116
126
  if (typeof cb !== "function") {
@@ -151,25 +161,16 @@ class PyRange {
151
161
  }
152
162
  return result;
153
163
  }
154
- /**
155
- * Reduces the range to a single value.
156
- * @param {function(T, number, number, PyRange): T} callback - The callback
157
- * function to apply to every element. The callback should take four
158
- * arguments: the accumulator, the current value, the index of the current
159
- * value, and the range object.
160
- * @param {T} [initialValue] - The initial value of the accumulator.
161
- * @returns {T} The final value of the accumulator.
162
- * @template T
163
- */
164
- reduce(callback, initialValue) {
164
+ reduce(callback, ...args) {
165
165
  PyRange.validateCb(callback);
166
- if (this._length === 0 && initialValue === undefined) {
166
+ const hasInitial = args.length > 0;
167
+ if (this._length === 0 && !hasInitial) {
167
168
  throw new TypeError("Reduce of empty range with no initial value");
168
169
  }
169
170
  let accumulator;
170
171
  let startIndex;
171
- if (initialValue !== undefined) {
172
- accumulator = initialValue;
172
+ if (hasInitial) {
173
+ accumulator = args[0];
173
174
  startIndex = 0;
174
175
  }
175
176
  else {
@@ -373,13 +374,22 @@ class PyRange {
373
374
  }
374
375
  /**
375
376
  * Reverses the order of the elements in this range, returning a new PyRange object.
377
+ *
378
+ * The new range starts at the last actual element of this range and steps
379
+ * in the opposite direction. The original range is not modified.
380
+ *
376
381
  * @returns {PyRange} A new PyRange object with the elements in reverse order.
377
382
  */
378
383
  reverse() {
379
- const result = new PyRange(this._stop, this._start, -this._step);
380
- // Force the length to be the same as the original range
381
- result._length = this._length;
382
- return result;
384
+ if (this._length === 0) {
385
+ // Return an empty range with the same step direction nothing to reverse
386
+ return new PyRange(this._start, this._start, this._step);
387
+ }
388
+ // The new start is the last actual element (not _stop which is exclusive)
389
+ const newStart = this.at(this._length - 1);
390
+ // The new stop is one step past the first element in the new direction
391
+ const newStop = this._start - this._step;
392
+ return new PyRange(newStart, newStop, -this._step);
383
393
  }
384
394
  /**
385
395
  * Returns an iterator of `[index, value]` pairs for each element in the range.
@@ -418,7 +428,7 @@ class PyRange {
418
428
  }
419
429
  /**
420
430
  * Implements the iterable protocol for this range.
421
- * @returns {Iterator<number>} An iterator for this range.
431
+ * @returns {IterableIterator<number>} An iterator for this range.
422
432
  */
423
433
  [Symbol.iterator]() {
424
434
  let index = 0;
@@ -436,6 +446,9 @@ class PyRange {
436
446
  return { value, done: false };
437
447
  }
438
448
  },
449
+ [Symbol.iterator]() {
450
+ return this;
451
+ },
439
452
  };
440
453
  }
441
454
  /**
@@ -443,7 +456,7 @@ class PyRange {
443
456
  * This proxy enables accessing range elements via array-like indexing.
444
457
  * If the property is a number, it will return the element at that index.
445
458
  *
446
- * @returns {PyRange & { [key: number]: number }} A proxy for the PyRange instance.
459
+ * @returns {PyRange & { [key: number]: number | undefined }} A proxy for the PyRange instance.
447
460
  */
448
461
  asProxy() {
449
462
  return new Proxy(this, {
@@ -451,8 +464,12 @@ class PyRange {
451
464
  if (typeof prop === "symbol") {
452
465
  return Reflect.get(target, prop, receiver);
453
466
  }
454
- if (!isNaN(Number(prop))) {
455
- return target.at(parseInt(String(prop), 10));
467
+ if (PyRange.isArrayIndexProperty(prop)) {
468
+ const idx = Number(prop);
469
+ // Match Array behaviour: out-of-bounds returns undefined, not throw
470
+ if (idx >= target.length)
471
+ return undefined;
472
+ return target.at(idx);
456
473
  }
457
474
  return Reflect.get(target, prop, receiver);
458
475
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "range-pie",
3
- "version": "2.4.1",
3
+ "version": "3.0.0",
4
4
  "description": "A TypeScript class that simulates Python's range function, combined with several useful JavaScript array methods.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -22,25 +22,27 @@
22
22
  ],
23
23
  "scripts": {
24
24
  "build": "tsc",
25
- "typecheck": "tsc --noEmit",
25
+ "typecheck": "tsc -p tsconfig.test.json --noEmit",
26
26
  "build:prod": "tsc -p tsconfig.prod.json",
27
27
  "build:watch": "tsc --watch",
28
28
  "prepare": "husky",
29
29
  "prepublishOnly": "npm run build:prod && npm run test",
30
30
  "test": "jest",
31
+ "test:watch": "jest --watch",
32
+ "test:coverage": "jest --coverage",
31
33
  "lint": "eslint . --ext .ts,.js",
32
34
  "lint:fix": "eslint . --ext .ts,.js --fix",
33
- "test:watch": "jest --watch",
34
35
  "clean": "rimraf dist",
35
- "format": "prettier --write \"src/**/*.{ts,js}\" \"test/**/*.{ts,js}\" \"examples/**/*.{ts,js}\" \"*.{js,json}\"",
36
- "format:check": "prettier --check \"src/**/*.{ts,js}\" \"test/**/*.{ts,js}\" \"examples/**/*.{ts,js}\" \"*.{js,json}\"",
36
+ "format": "prettier --write .",
37
+ "format:check": "prettier --check .",
37
38
  "example:basic": "node examples/basic-usage.js",
38
39
  "example:array": "node examples/array-methods.js",
39
40
  "example:advanced": "node examples/advanced-usage.js",
40
41
  "example:ts": "ts-node examples/typescript-usage.ts",
41
42
  "example:methods": "for file in examples/methods/*.ts; do echo \"\\n=== Running $file ===\"; ts-node \"$file\"; done",
42
43
  "knip": "knip",
43
- "precommit": "npm run typecheck && npx --no-install lint-staged"
44
+ "lint-staged": "lint-staged",
45
+ "precommit": "npm run typecheck && npm run lint-staged"
44
46
  },
45
47
  "keywords": [
46
48
  "range",
@@ -60,20 +62,23 @@
60
62
  "url": "git+https://github.com/SkorpionG/range-pie.git"
61
63
  },
62
64
  "devDependencies": {
63
- "@eslint/js": "^9.39.4",
65
+ "@eslint/js": "^10.0.1",
64
66
  "@types/jest": "^30.0.0",
65
- "@types/node": "^25.4.0",
66
- "eslint": "^9.39.4",
67
- "globals": "^17.4.0",
67
+ "@types/node": "^26.0.1",
68
+ "eslint": "^10.6.0",
69
+ "globals": "^17.7.0",
68
70
  "husky": "^9.1.7",
69
- "jest": "^30.3.0",
70
- "knip": "^5.86.0",
71
- "lint-staged": "^16.3.3",
72
- "prettier": "^3.8.1",
71
+ "jest": "^30.4.2",
72
+ "knip": "^6.23.0",
73
+ "lint-staged": "^17.0.8",
74
+ "prettier": "^3.9.1",
73
75
  "rimraf": "^6.1.3",
74
- "ts-jest": "^29.4.6",
75
- "ts-node": "^10.9.1",
76
- "typescript": "^5.9.3",
77
- "typescript-eslint": "^8.57.0"
76
+ "ts-jest": "^29.4.11",
77
+ "ts-node": "^10.9.2",
78
+ "typescript": "^6.0.3",
79
+ "typescript-eslint": "^8.62.0"
80
+ },
81
+ "overrides": {
82
+ "js-yaml": "^4.1.2"
78
83
  }
79
84
  }