array-view-ts
276 строк · 7.4 Кб
1import { IndexError, ValueError } from "./excpetions";
2import { normalizeIndex } from "./utils";
3
4/**
5* Represents a slice definition for selecting a range of elements.
6*/
7export class Slice {
8/**
9* The start index of the slice range.
10*/
11public readonly start: number | undefined;
12/**
13* The end index of the slice range.
14*/
15public readonly end: number | undefined;
16/**
17* The step size for selecting elements in the slice range.
18*/
19public readonly step: number | undefined;
20
21/**
22* Converts a slice string or Slice object into a Slice instance.
23*
24* @param {string | Array<number | undefined> | Slice} s - The slice string/array or Slice object to convert.
25* @returns {Slice} The converted Slice instance.
26*/
27public static toSlice(s: string | Array<number | undefined> | Slice): Slice {
28if (s instanceof Slice) {
29return s;
30}
31
32if (Array.isArray(s) && this.isSliceArray(s)) {
33return new Slice(...(s as Array<number | undefined>));
34}
35
36if (!this.isSliceString(s)) {
37throw new ValueError(`Invalid slice: "${String(s)}".`);
38}
39
40const slice = this.parseSliceString(s as string);
41
42return new Slice(...slice);
43}
44
45/**
46* Checks if the provided value is a Slice instance or a valid slice string.
47*
48* @param {unknown} s - The value to check.
49*
50* @returns {boolean} True if the value is a Slice instance or a valid slice string, false otherwise.
51*/
52public static isSlice(s: unknown): boolean {
53return (s instanceof Slice) || this.isSliceString(s);
54}
55
56/**
57* Checks if the provided value is a valid slice string.
58*
59* @param {unknown} s - The value to check.
60*
61* @returns {boolean} True if the value is a valid slice string, false otherwise.
62*/
63public static isSliceString(s: unknown): boolean {
64if (typeof s !== "string") {
65return false;
66}
67
68if (!Number.isNaN(Number(s))) {
69return false;
70}
71
72if (!s.match(/^-?[0-9]*:?-?[0-9]*:?-?[0-9]*$/)) {
73return false;
74}
75
76const slice = this.parseSliceString(s);
77
78return slice.length >= 1 && slice.length <= 3;
79}
80
81/**
82* Checks if the provided value is a valid slice array.
83*
84* @param {unknown} s - The value to check.
85*
86* @returns {boolean} True if the value is a valid slice array, false otherwise.
87*/
88public static isSliceArray(s: unknown): boolean {
89if (!Array.isArray(s)) {
90return false;
91}
92
93if(!(s.length >= 0 && s.length <= 3)) {
94return false;
95}
96
97for (const item of s) {
98if (item !== undefined && !Number.isInteger(item)) {
99return false;
100}
101}
102
103return true;
104}
105
106/**
107* Creates a new Slice instance with optional start, end, and step values.
108*
109* @param {number} [start] - The start index of the slice range.
110* @param {number} [end] - The end index of the slice range.
111* @param {number} [step] - The step size for selecting elements in the slice range.
112*/
113constructor(start?: number, end?: number, step?: number) {
114this.start = start;
115this.end = end;
116this.step = step;
117}
118
119/**
120* Normalizes the slice parameters based on the container length.
121*
122* @param {number} containerLength - The length of the container or array.
123*
124* @returns {NormalizedSlice} The normalized slice parameters.
125*/
126public normalize(containerLength: number): NormalizedSlice {
127let step = this.step ?? 1;
128
129if (step > 0) {
130let start = this.start ?? 0;
131let end = this.end ?? containerLength;
132
133[start, end, step] = [Math.round(start), Math.round(end), Math.round(step)];
134
135start = normalizeIndex(start, containerLength, false);
136end = normalizeIndex(end, containerLength, false);
137
138if (start >= containerLength) {
139start = end = containerLength - 1;
140}
141
142start = this.squeezeInBounds(start, 0, containerLength - 1);
143end = this.squeezeInBounds(end, 0, containerLength);
144
145if (end < start) {
146end = start;
147}
148
149return new NormalizedSlice(start, end, step);
150} else if (step < 0) {
151let start = this.start ?? containerLength - 1;
152let end = this.end ?? -1;
153
154[start, end, step] = [Math.round(start), Math.round(end), Math.round(step)];
155
156start = normalizeIndex(start, containerLength, false);
157
158if (!(this.end === undefined)) {
159end = normalizeIndex(end, containerLength, false);
160}
161
162if (start < 0) {
163start = end = 0;
164}
165
166start = this.squeezeInBounds(start, 0, containerLength - 1);
167end = this.squeezeInBounds(end, -1, containerLength);
168
169if (end > start) {
170end = start;
171}
172
173return new NormalizedSlice(start, end, step);
174}
175
176throw new IndexError("Step cannot be 0.");
177}
178
179/**
180* Returns the string representation of the Slice.
181*
182* @returns {string} The string representation of the Slice.
183*/
184public toString(): string {
185return `${this.start ?? ""}:${this.end ?? ""}:${this.step ?? ""}`;
186}
187
188/**
189* Parses a slice string into an array of start, end, and step values.
190*
191* @param {string} s - The slice string to parse.
192*
193* @returns {(number | undefined)[]} An array of parsed start, end, and step values.
194*/
195private static parseSliceString(s: string): (number | undefined)[] {
196return s.split(":")
197.map(x => x.trim())
198.map(x => (x === "") ? undefined : parseInt(x));
199}
200
201/**
202* Constrains a value within a given range.
203*
204* @param {number} x - The value to constrain.
205* @param {number} min - The minimum allowed value.
206* @param {number} max - The maximum allowed value.
207*
208* @returns {number} The constrained value.
209*/
210private squeezeInBounds(x: number, min: number, max: number): number {
211return Math.max(min, Math.min(max, x));
212}
213}
214
215/**
216* Represents a normalized slice definition with start, end, and step values.
217*/
218export class NormalizedSlice extends Slice {
219/**
220* The start index of the normalized slice.
221*/
222public readonly start: number;
223/**
224* The end index of the normalized slice.
225*/
226public readonly end: number;
227/**
228* The step size for selecting elements in the normalized slice.
229*/
230public readonly step: number;
231
232/**
233* Creates a new NormalizedSlice instance with start, end, and step values.
234*
235* @param {number} start - The start index of the normalized slice.
236* @param {number} end - The end index of the normalized slice.
237* @param {number} step - The step size for selecting elements in the normalized slice.
238*/
239constructor(start: number, end: number, step: number) {
240super();
241this.start = start;
242this.end = end;
243this.step = step;
244}
245
246/**
247* Returns the length of the normalized slice.
248*
249* @type {number}
250*/
251get length(): number {
252return Math.ceil(Math.abs(((this.end - this.start) / this.step)));
253}
254
255/**
256* Converts the provided index to the actual index based on the normalized slice parameters.
257*
258* @param {number} i - The index to convert.
259*
260* @returns {number} The converted index value.
261*/
262public convertIndex(i: number): number {
263return this.start + normalizeIndex(i, this.length, false) * this.step;
264}
265
266/**
267* Generate an iterator for iterating over the elements in the normalized slice range.
268*
269* @returns {IterableIterator<number>} An iterator for the normalized slice range.
270*/
271public* toRange(): IterableIterator<number> {
272for (let i = 0; i < this.length; ++i) {
273yield this.convertIndex(i);
274}
275}
276}
277