From 1dbbcc956d7c2fb5ff0f333ca474ffde69d261ae Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:29:07 -0700 Subject: [PATCH] feat: add shift method to Series Implements Series.shift(periods, options) matching the pandas API. Supports positive/negative periods and custom fillValue. Includes tests for all edge cases. Fixes #617 --- src/danfojs-base/core/series.ts | 55 +++++++++++++++++++++++ src/danfojs-node/test/core/series.test.ts | 36 ++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/danfojs-base/core/series.ts b/src/danfojs-base/core/series.ts index 9db5f37a..33c759a3 100644 --- a/src/danfojs-base/core/series.ts +++ b/src/danfojs-base/core/series.ts @@ -192,6 +192,61 @@ export default class Series extends NDframe implements SeriesInterface { return this.iloc([`${startIdx}:`]) } + /** + * Shift index by desired number of periods. Shifted data is filled with NaN (or fillValue). + * @param periods Number of periods to shift. Positive shifts down, negative shifts up. + * @param options Optional. An object with a `fillValue` property for filling shifted positions. + * @example + * ``` + * const sf = new Series([1, 2, 3, 4, 5]); + * const sf2 = sf.shift(1); + * console.log(sf2.values); // [NaN, 1, 2, 3, 4] + * ``` + * @example + * ``` + * const sf = new Series([1, 2, 3, 4, 5]); + * const sf2 = sf.shift(-1); + * console.log(sf2.values); // [2, 3, 4, 5, NaN] + * ``` + */ + shift(periods: number = 1, options?: { fillValue?: number | string | boolean }): Series { + const fillValue = options?.fillValue !== undefined ? options.fillValue : NaN; + const values = this.values as ArrayType1D; + const len = values.length; + const newValues: ArrayType1D = new Array(len).fill(fillValue); + + if (periods === 0) { + return this.copy(); + } + + if (Math.abs(periods) >= len) { + return new Series(newValues, { + index: [...this.index], + columns: this.columns, + dtypes: this.dtypes, + config: { ...this.config } + }); + } + + if (periods > 0) { + for (let i = periods; i < len; i++) { + newValues[i] = values[i - periods]; + } + } else { + const absPeriods = Math.abs(periods); + for (let i = 0; i < len - absPeriods; i++) { + newValues[i] = values[i + absPeriods]; + } + } + + return new Series(newValues, { + index: [...this.index], + columns: this.columns, + dtypes: this.dtypes, + config: { ...this.config } + }); + } + /** * Returns specified number of random rows in a Series * @param num The number of rows to return diff --git a/src/danfojs-node/test/core/series.test.ts b/src/danfojs-node/test/core/series.test.ts index ac17d092..34e0423e 100644 --- a/src/danfojs-node/test/core/series.test.ts +++ b/src/danfojs-node/test/core/series.test.ts @@ -1755,4 +1755,38 @@ describe("Series Functions", () => { }); }); -}) \ No newline at end of file + + describe("shift", function () { + it("Shifts values down by 1 with NaN fill", function () { + const sf = new Series([1, 2, 3, 4, 5]); + const result = sf.shift(1); + assert.deepEqual(result.values[0], NaN); + assert.deepEqual(result.values.slice(1), [1, 2, 3, 4]); + }); + + it("Shifts values up by -1 with NaN fill", function () { + const sf = new Series([1, 2, 3, 4, 5]); + const result = sf.shift(-1); + assert.deepEqual(result.values.slice(0, 4), [2, 3, 4, 5]); + assert.deepEqual(result.values[4], NaN); + }); + + it("Returns copy when periods is 0", function () { + const sf = new Series([1, 2, 3]); + const result = sf.shift(0); + assert.deepEqual(result.values, sf.values); + }); + + it("Uses custom fillValue", function () { + const sf = new Series([1, 2, 3, 4]); + const result = sf.shift(2, { fillValue: 0 }); + assert.deepEqual(result.values, [0, 0, 1, 2]); + }); + + it("Returns all fill values when shift exceeds length", function () { + const sf = new Series([1, 2, 3]); + const result = sf.shift(5); + assert.ok(result.values.every((v: any) => isNaN(v))); + }); + }); +})