diff --git a/src/methods/series/filtering/between.js b/src/methods/series/filtering/between.js new file mode 100644 index 0000000..1aa0be0 --- /dev/null +++ b/src/methods/series/filtering/between.js @@ -0,0 +1,54 @@ +/** + * Between method for Series + * Returns a new Series with values between lower and upper bounds (inclusive) + */ + +/** + * Creates a between method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function between() { + /** + * Returns a new Series with values between lower and upper bounds (inclusive) + * @param {number} lower - Lower bound + * @param {number} upper - Upper bound + * @param {Object} [options] - Options object + * @param {boolean} [options.inclusive=true] - Whether bounds are inclusive + * @returns {Series} - New Series with filtered values + */ + return function(lower, upper, options = {}) { + const { inclusive = true } = options; + + if (lower === undefined || upper === undefined) { + throw new Error('Both lower and upper bounds must be provided'); + } + + if (lower > upper) { + throw new Error('Lower bound must be less than or equal to upper bound'); + } + + if (inclusive) { + return this.filter((x) => { + const numX = Number(x); + return !isNaN(numX) && numX >= lower && numX <= upper; + }); + } else { + return this.filter((x) => { + const numX = Number(x); + return !isNaN(numX) && numX > lower && numX < upper; + }); + } + }; +} + +/** + * Registers the between method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.between) { + Series.prototype.between = between(); + } +} + +export default between; diff --git a/src/methods/series/filtering/contains.js b/src/methods/series/filtering/contains.js new file mode 100644 index 0000000..2fd6aa5 --- /dev/null +++ b/src/methods/series/filtering/contains.js @@ -0,0 +1,51 @@ +/** + * Contains method for Series + * Returns a new Series with string values that contain the specified substring + */ + +/** + * Creates a contains method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function contains() { + /** + * Returns a new Series with string values that contain the specified substring + * @param {string} substring - Substring to search for + * @param {Object} [options] - Options object + * @param {boolean} [options.caseSensitive=true] - Whether the search is case sensitive + * @returns {Series} - New Series with filtered values + */ + return function(substring, options = {}) { + const { caseSensitive = true } = options; + + if (substring === undefined || substring === null) { + throw new Error('Substring must be provided'); + } + + return this.filter((value) => { + if (value === null || value === undefined) { + return false; + } + + const strValue = String(value); + + if (caseSensitive) { + return strValue.includes(substring); + } else { + return strValue.toLowerCase().includes(substring.toLowerCase()); + } + }); + }; +} + +/** + * Registers the contains method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.contains) { + Series.prototype.contains = contains(); + } +} + +export default contains; diff --git a/src/methods/series/filtering/endsWith.js b/src/methods/series/filtering/endsWith.js new file mode 100644 index 0000000..4fe9fde --- /dev/null +++ b/src/methods/series/filtering/endsWith.js @@ -0,0 +1,52 @@ +/** + * EndsWith method for Series + * Returns a new Series with string values that end with the specified suffix + */ + +/** + * Creates an endsWith method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function endsWith() { + /** + * Returns a new Series with string values that end with the specified suffix + * @param {string} suffix - Suffix to search for + * @param {Object} [options] - Options object + * @param {boolean} [options.caseSensitive=true] - Whether the search is case sensitive + * @returns {Series} - New Series with filtered values + */ + return function(suffix, options = {}) { + const { caseSensitive = true } = options; + + if (suffix === undefined || suffix === null) { + throw new Error('Suffix must be provided'); + } + + return this.filter((value) => { + if (value === null || value === undefined) { + return false; + } + + const strValue = String(value); + + if (caseSensitive) { + // В режиме чувствительности к регистру проверяем точное совпадение + return strValue.endsWith(suffix); + } else { + return strValue.toLowerCase().endsWith(suffix.toLowerCase()); + } + }); + }; +} + +/** + * Registers the endsWith method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.endsWith) { + Series.prototype.endsWith = endsWith(); + } +} + +export default endsWith; diff --git a/src/methods/series/filtering/filter.js b/src/methods/series/filtering/filter.js index aee8217..8535bc1 100644 --- a/src/methods/series/filtering/filter.js +++ b/src/methods/series/filtering/filter.js @@ -5,20 +5,22 @@ * @param {Function} predicate - Function that takes a value and returns true/false * @returns {Series} - New Series with filtered values */ -export const filter = (series, predicate) => { +export function filter(series, predicate) { const values = series.toArray(); const filteredValues = values.filter(predicate); return new series.constructor(filteredValues); -}; +} /** * Registers the filter method on Series prototype * @param {Class} Series - Series class to extend */ -export const register = (Series) => { - Series.prototype.filter = function(predicate) { - return filter(this, predicate); - }; -}; +export function register(Series) { + if (!Series.prototype.filter) { + Series.prototype.filter = function(predicate) { + return filter(this, predicate); + }; + } +} export default { filter, register }; diff --git a/src/methods/series/filtering/isNull.js b/src/methods/series/filtering/isNull.js new file mode 100644 index 0000000..c8c55a1 --- /dev/null +++ b/src/methods/series/filtering/isNull.js @@ -0,0 +1,30 @@ +/** + * IsNull method for Series + * Returns a new Series with only null or undefined values + */ + +/** + * Creates an isNull method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function isNull() { + /** + * Returns a new Series with only null or undefined values + * @returns {Series} - New Series with filtered values + */ + return function() { + return this.filter((x) => x === null || x === undefined); + }; +} + +/** + * Registers the isNull method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.isNull) { + Series.prototype.isNull = isNull(); + } +} + +export default isNull; diff --git a/src/methods/series/filtering/matches.js b/src/methods/series/filtering/matches.js new file mode 100644 index 0000000..5ea67f7 --- /dev/null +++ b/src/methods/series/filtering/matches.js @@ -0,0 +1,51 @@ +/** + * Matches method for Series + * Returns a new Series with string values that match the specified regular expression + */ + +/** + * Creates a matches method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function matches() { + /** + * Returns a new Series with string values that match the specified regular expression + * @param {RegExp|string} pattern - Regular expression pattern to match + * @param {Object} [options] - Options object + * @param {boolean} [options.flags] - Flags for the RegExp if pattern is a string + * @returns {Series} - New Series with filtered values + */ + return function(pattern, options = {}) { + const { flags = '' } = options; + + if (pattern === undefined || pattern === null) { + throw new Error('Regular expression pattern must be provided'); + } + + // Convert string pattern to RegExp if needed + const regex = pattern instanceof RegExp + ? pattern + : new RegExp(pattern, flags); + + return this.filter((value) => { + if (value === null || value === undefined) { + return false; + } + + const strValue = String(value); + return regex.test(strValue); + }); + }; +} + +/** + * Registers the matches method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.matches) { + Series.prototype.matches = matches(); + } +} + +export default matches; diff --git a/src/methods/series/filtering/register.js b/src/methods/series/filtering/register.js index bb1e02f..e550195 100644 --- a/src/methods/series/filtering/register.js +++ b/src/methods/series/filtering/register.js @@ -2,93 +2,135 @@ * Registrar for Series filtering methods */ +import { register as registerBetween } from './between.js'; +import { register as registerContains } from './contains.js'; +import { register as registerStartsWith } from './startsWith.js'; +import { register as registerEndsWith } from './endsWith.js'; +import { register as registerMatches } from './matches.js'; +import { register as registerIsNull } from './isNull.js'; + /** * Registers all filtering methods for Series * @param {Class} Series - Series class to extend */ export function registerSeriesFiltering(Series) { - /** - * Filters elements in a Series based on a predicate function - * @param {Function} predicate - Function that takes a value and returns true/false - * @returns {Series} - New Series with filtered values - */ - Series.prototype.filter = function(predicate) { - const values = this.toArray(); - const filteredValues = values.filter(predicate); - return new this.constructor(filteredValues); - }; + // Only register filter if it's not already registered + if (!Series.prototype.filter) { + /** + * Filters elements in a Series based on a predicate function + * @param {Function} predicate - Function that takes a value and returns true/false + * @returns {Series} - New Series with filtered values + */ + Series.prototype.filter = function(predicate) { + const values = this.toArray(); + const filteredValues = values.filter(predicate); + return new this.constructor(filteredValues); + }; + } + + // Only register gt if it's not already registered + if (!Series.prototype.gt) { + /** + * Returns a new Series with values greater than the specified value + * @param {number} value - Value to compare against + * @returns {Series} - New Series with filtered values + */ + Series.prototype.gt = function(value) { + return this.filter((x) => x > value); + }; + } - /** - * Returns a new Series with values greater than the specified value - * @param {number} value - Value to compare against - * @returns {Series} - New Series with filtered values - */ - Series.prototype.gt = function(value) { - return this.filter((x) => x > value); - }; + // Only register gte if it's not already registered + if (!Series.prototype.gte) { + /** + * Returns a new Series with values greater than or equal to the specified value + * @param {number} value - Value to compare against + * @returns {Series} - New Series with filtered values + */ + Series.prototype.gte = function(value) { + return this.filter((x) => x >= value); + }; + } - /** - * Returns a new Series with values greater than or equal to the specified value - * @param {number} value - Value to compare against - * @returns {Series} - New Series with filtered values - */ - Series.prototype.gte = function(value) { - return this.filter((x) => x >= value); - }; + // Only register lt if it's not already registered + if (!Series.prototype.lt) { + /** + * Returns a new Series with values less than the specified value + * @param {number} value - Value to compare against + * @returns {Series} - New Series with filtered values + */ + Series.prototype.lt = function(value) { + return this.filter((x) => x < value); + }; + } - /** - * Returns a new Series with values less than the specified value - * @param {number} value - Value to compare against - * @returns {Series} - New Series with filtered values - */ - Series.prototype.lt = function(value) { - return this.filter((x) => x < value); - }; + // Only register lte if it's not already registered + if (!Series.prototype.lte) { + /** + * Returns a new Series with values less than or equal to the specified value + * @param {number} value - Value to compare against + * @returns {Series} - New Series with filtered values + */ + Series.prototype.lte = function(value) { + return this.filter((x) => x <= value); + }; + } - /** - * Returns a new Series with values less than or equal to the specified value - * @param {number} value - Value to compare against - * @returns {Series} - New Series with filtered values - */ - Series.prototype.lte = function(value) { - return this.filter((x) => x <= value); - }; + // Only register eq if it's not already registered + if (!Series.prototype.eq) { + /** + * Returns a new Series with values equal to the specified value + * @param {*} value - Value to compare against + * @returns {Series} - New Series with filtered values + */ + Series.prototype.eq = function(value) { + return this.filter((x) => x === value); + }; + } - /** - * Returns a new Series with values equal to the specified value - * @param {*} value - Value to compare against - * @returns {Series} - New Series with filtered values - */ - Series.prototype.eq = function(value) { - return this.filter((x) => x === value); - }; + // Only register ne if it's not already registered + if (!Series.prototype.ne) { + /** + * Returns a new Series with values not equal to the specified value + * @param {*} value - Value to compare against + * @returns {Series} - New Series with filtered values + */ + Series.prototype.ne = function(value) { + return this.filter((x) => x !== value); + }; + } - /** - * Returns a new Series with values not equal to the specified value - * @param {*} value - Value to compare against - * @returns {Series} - New Series with filtered values - */ - Series.prototype.ne = function(value) { - return this.filter((x) => x !== value); - }; + // Only register notNull if it's not already registered + if (!Series.prototype.notNull) { + /** + * Returns a new Series with non-null values + * @returns {Series} - New Series with non-null values + */ + Series.prototype.notNull = function() { + return this.filter((x) => x !== null && x !== undefined); + }; + } - /** - * Returns a new Series with non-null values - * @returns {Series} - New Series with non-null values - */ - Series.prototype.notNull = function() { - return this.filter((x) => x !== null && x !== undefined); - }; + // Only register isin if it's not already registered + if (!Series.prototype.isin) { + /** + * Returns a new Series with values in the specified array + * @param {Array} values - Array of values to include + * @returns {Series} - New Series with filtered values + */ + Series.prototype.isin = function(values) { + const valueSet = new Set(values); + return this.filter((x) => valueSet.has(x)); + }; + } - /** - * Returns a new Series with values in the specified array - * @param {Array} values - Array of values to include - * @returns {Series} - New Series with filtered values - */ - Series.prototype.isin = function(values) { - const valueSet = new Set(values); - return this.filter((x) => valueSet.has(x)); - }; + // Register additional filtering methods + registerBetween(Series); + registerContains(Series); + registerStartsWith(Series); + registerEndsWith(Series); + registerMatches(Series); + registerIsNull(Series); } export default registerSeriesFiltering; diff --git a/src/methods/series/filtering/startsWith.js b/src/methods/series/filtering/startsWith.js new file mode 100644 index 0000000..ec083ca --- /dev/null +++ b/src/methods/series/filtering/startsWith.js @@ -0,0 +1,51 @@ +/** + * StartsWith method for Series + * Returns a new Series with string values that start with the specified prefix + */ + +/** + * Creates a startsWith method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function startsWith() { + /** + * Returns a new Series with string values that start with the specified prefix + * @param {string} prefix - Prefix to search for + * @param {Object} [options] - Options object + * @param {boolean} [options.caseSensitive=true] - Whether the search is case sensitive + * @returns {Series} - New Series with filtered values + */ + return function(prefix, options = {}) { + const { caseSensitive = true } = options; + + if (prefix === undefined || prefix === null) { + throw new Error('Prefix must be provided'); + } + + return this.filter((value) => { + if (value === null || value === undefined) { + return false; + } + + const strValue = String(value); + + if (caseSensitive) { + return strValue.startsWith(prefix); + } else { + return strValue.toLowerCase().startsWith(prefix.toLowerCase()); + } + }); + }; +} + +/** + * Registers the startsWith method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.startsWith) { + Series.prototype.startsWith = startsWith(); + } +} + +export default startsWith; diff --git a/src/methods/series/transform/clip.js b/src/methods/series/transform/clip.js new file mode 100644 index 0000000..65b913d --- /dev/null +++ b/src/methods/series/transform/clip.js @@ -0,0 +1,78 @@ +/** + * Clip method for Series + * Returns a new Series with values clipped to specified min and max + */ + +/** + * Creates a clip method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function clip() { + /** + * Returns a new Series with values clipped to specified min and max + * @param {Object} [options] - Options object + * @param {number} [options.min] - Minimum value + * @param {number} [options.max] - Maximum value + * @param {boolean} [options.inplace=false] - Modify the Series in place + * @returns {Series} - New Series with clipped values + */ + return function(options = {}) { + const { min = undefined, max = undefined, inplace = false } = options; + + if (min === undefined && max === undefined) { + throw new Error('At least one of min or max must be provided'); + } + + const values = this.toArray(); + const result = new Array(values.length); + + for (let i = 0; i < values.length; i++) { + const value = values[i]; + + if (value === null || value === undefined) { + result[i] = value; + continue; + } + + if (typeof value !== 'number' || Number.isNaN(value)) { + result[i] = value; + continue; + } + + let clippedValue = value; + + if (min !== undefined && value < min) { + clippedValue = min; + } + + if (max !== undefined && value > max) { + clippedValue = max; + } + + result[i] = clippedValue; + } + + if (inplace) { + // Replace the values in the current Series + // Поскольку метода set нет, создаем новый объект Series и заменяем внутренние свойства + const newSeries = new this.constructor(result, { name: this.name }); + Object.assign(this, newSeries); + return this; + } else { + // Create a new Series with the clipped values + return new this.constructor(result, { name: this.name }); + } + }; +} + +/** + * Registers the clip method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.clip) { + Series.prototype.clip = clip(); + } +} + +export default clip; diff --git a/src/methods/series/transform/diff.js b/src/methods/series/transform/diff.js new file mode 100644 index 0000000..b3fb558 --- /dev/null +++ b/src/methods/series/transform/diff.js @@ -0,0 +1,71 @@ +/** + * Diff method for Series + * Returns a new Series with the difference between consecutive elements + */ + +/** + * Creates a diff method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function diff() { + /** + * Returns a new Series with the difference between consecutive elements + * @param {Object} [options] - Options object + * @param {number} [options.periods=1] - Number of periods to shift for calculating difference + * @returns {Series} - New Series with differences + */ + return function(options = {}) { + const { periods = 1 } = options; + + if (!Number.isInteger(periods) || periods < 1) { + throw new Error('Periods must be a positive integer'); + } + + const values = this.toArray(); + + // Обработка пустого массива - возвращаем пустой массив + if (values.length === 0) { + return new this.constructor([], { name: this.name }); + } + + const result = new Array(values.length); + + // First N elements will be NaN (where N is the number of periods) + for (let i = 0; i < periods && i < values.length; i++) { + result[i] = NaN; + } + + // Calculate differences for the rest + for (let i = periods; i < values.length; i++) { + const currentValue = values[i]; + const previousValue = values[i - periods]; + + // Проверка на строки, которые можно преобразовать в числа + const numCurrent = typeof currentValue === 'string' ? Number(currentValue) : currentValue; + const numPrevious = typeof previousValue === 'string' ? Number(previousValue) : previousValue; + + if (numCurrent === null || numCurrent === undefined || + numPrevious === null || numPrevious === undefined || + typeof numCurrent !== 'number' || typeof numPrevious !== 'number' || + Number.isNaN(numCurrent) || Number.isNaN(numPrevious)) { + result[i] = NaN; + } else { + result[i] = numCurrent - numPrevious; + } + } + + return new this.constructor(result, { name: this.name }); + }; +} + +/** + * Registers the diff method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.diff) { + Series.prototype.diff = diff(); + } +} + +export default diff; diff --git a/src/methods/series/transform/dropna.js b/src/methods/series/transform/dropna.js new file mode 100644 index 0000000..a5c63a7 --- /dev/null +++ b/src/methods/series/transform/dropna.js @@ -0,0 +1,30 @@ +/** + * DropNA method for Series + * Returns a new Series with null/undefined values removed + */ + +/** + * Creates a dropna method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function dropna() { + /** + * Returns a new Series with null/undefined values removed + * @returns {Series} - New Series without null/undefined values + */ + return function() { + return this.filter((value) => value !== null && value !== undefined); + }; +} + +/** + * Registers the dropna method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.dropna) { + Series.prototype.dropna = dropna(); + } +} + +export default dropna; diff --git a/src/methods/series/transform/fillna.js b/src/methods/series/transform/fillna.js new file mode 100644 index 0000000..69ba7a3 --- /dev/null +++ b/src/methods/series/transform/fillna.js @@ -0,0 +1,55 @@ +/** + * FillNA method for Series + * Returns a new Series with null/undefined values filled + */ + +/** + * Creates a fillna method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function fillna() { + /** + * Returns a new Series with null/undefined values filled + * @param {*} value - Value to fill null/undefined values with + * @param {Object} [options] - Options object + * @param {boolean} [options.inplace=false] - Modify the Series in place + * @returns {Series} - New Series with filled values + */ + return function(value, options = {}) { + const { inplace = false } = options; + + if (value === undefined) { + throw new Error('Fill value must be provided'); + } + + const values = this.toArray(); + const result = new Array(values.length); + + for (let i = 0; i < values.length; i++) { + result[i] = values[i] === null || values[i] === undefined ? value : values[i]; + } + + if (inplace) { + // Replace the values in the current Series + // Поскольку метода set нет, создаем новый объект Series и заменяем внутренние свойства + const newSeries = new this.constructor(result, { name: this.name }); + Object.assign(this, newSeries); + return this; + } else { + // Create a new Series with the filled values + return new this.constructor(result, { name: this.name }); + } + }; +} + +/** + * Registers the fillna method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.fillna) { + Series.prototype.fillna = fillna(); + } +} + +export default fillna; diff --git a/src/methods/series/transform/pctChange.js b/src/methods/series/transform/pctChange.js new file mode 100644 index 0000000..f9c4b1b --- /dev/null +++ b/src/methods/series/transform/pctChange.js @@ -0,0 +1,70 @@ +/** + * Percent change method for Series + * Returns a new Series with the percentage change between consecutive elements + */ + +/** + * Creates a pctChange method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function pctChange() { + /** + * Returns a new Series with the percentage change between consecutive elements + * @param {Object} [options] - Options object + * @param {number} [options.periods=1] - Number of periods to shift for calculating percentage change + * @param {boolean} [options.fill=null] - Value to use for filling NA/NaN values + * @returns {Series} - New Series with percentage changes + */ + return function(options = {}) { + const { periods = 1, fill = null } = options; + + if (!Number.isInteger(periods) || periods < 1) { + throw new Error('Periods must be a positive integer'); + } + + const values = this.toArray(); + + // Обработка пустого массива - возвращаем пустой массив + if (values.length === 0) { + return new this.constructor([], { name: this.name }); + } + + const result = new Array(values.length); + + // First N elements will be NaN (where N is the number of periods) + for (let i = 0; i < periods && i < values.length; i++) { + result[i] = fill; + } + + // Calculate percentage changes for the rest + for (let i = periods; i < values.length; i++) { + const currentValue = values[i]; + const previousValue = values[i - periods]; + + if (currentValue === null || currentValue === undefined || + previousValue === null || previousValue === undefined || + typeof currentValue !== 'number' || typeof previousValue !== 'number' || + Number.isNaN(currentValue) || Number.isNaN(previousValue) || + previousValue === 0) { + result[i] = fill; + } else { + // Правильный расчет процентного изменения для отрицательных значений + result[i] = (currentValue - previousValue) / Math.abs(previousValue); + } + } + + return new this.constructor(result, { name: this.name }); + }; +} + +/** + * Registers the pctChange method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.pctChange) { + Series.prototype.pctChange = pctChange(); + } +} + +export default pctChange; diff --git a/src/methods/series/transform/register.js b/src/methods/series/transform/register.js index 0196d0f..d01b822 100644 --- a/src/methods/series/transform/register.js +++ b/src/methods/series/transform/register.js @@ -2,6 +2,16 @@ * Registrar for Series transformation methods */ +// Import all transformation methods +import { sort } from './sort.js'; +import { unique } from './unique.js'; +import { replace } from './replace.js'; +import { fillna } from './fillna.js'; +import { dropna } from './dropna.js'; +import { clip } from './clip.js'; +import { diff } from './diff.js'; +import { pct_change } from './pct_change.js'; + /** * Registers all transformation methods for Series * @param {Class} Series - Series class to extend @@ -103,7 +113,38 @@ export function registerSeriesTransform(Series) { return this.map(fn); }; - // Here you can add other transformation methods + // Register new transformation methods + if (!Series.prototype.sort) { + Series.prototype.sort = sort(); + } + + if (!Series.prototype.unique) { + Series.prototype.unique = unique(); + } + + if (!Series.prototype.replace) { + Series.prototype.replace = replace(); + } + + if (!Series.prototype.fillna) { + Series.prototype.fillna = fillna(); + } + + if (!Series.prototype.dropna) { + Series.prototype.dropna = dropna(); + } + + if (!Series.prototype.clip) { + Series.prototype.clip = clip(); + } + + if (!Series.prototype.diff) { + Series.prototype.diff = diff(); + } + + if (!Series.prototype.pct_change) { + Series.prototype.pct_change = pct_change(); + } } export default registerSeriesTransform; diff --git a/src/methods/series/transform/replace.js b/src/methods/series/transform/replace.js new file mode 100644 index 0000000..e051d72 --- /dev/null +++ b/src/methods/series/transform/replace.js @@ -0,0 +1,84 @@ +/** + * Replace method for Series + * Returns a new Series with replaced values + */ + +/** + * Creates a replace method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function replace() { + /** + * Returns a new Series with replaced values + * @param {*} oldValue - Value to replace + * @param {*} newValue - New value + * @param {Object} [options] - Options object + * @param {boolean} [options.regex=false] - Whether to treat oldValue as a regular expression + * @param {boolean} [options.inplace=false] - Modify the Series in place + * @returns {Series} - New Series with replaced values + */ + return function(oldValue, newValue, options = {}) { + const { regex = false, inplace = false } = options; + + if (oldValue === undefined) { + throw new Error('Old value must be provided'); + } + + if (newValue === undefined) { + throw new Error('New value must be provided'); + } + + const values = this.toArray(); + const result = new Array(values.length); + + if (regex && typeof oldValue === 'string') { + // Create a RegExp object from the string pattern + const pattern = new RegExp(oldValue); + + for (let i = 0; i < values.length; i++) { + const value = values[i]; + + if (value === null || value === undefined) { + result[i] = value; + continue; + } + + const strValue = String(value); + if (pattern.test(strValue)) { + result[i] = newValue; + } else { + result[i] = value; + } + } + } else { + // Direct value comparison + for (let i = 0; i < values.length; i++) { + result[i] = Object.is(values[i], oldValue) ? newValue : values[i]; + } + } + + if (inplace) { + // Replace the values in the current Series + // Поскольку метода set нет, создаем новый массив и заменяем внутренний массив values + // через Object.assign + const newSeries = new this.constructor(result, { name: this.name }); + Object.assign(this, newSeries); + return this; + } else { + // Create a new Series with the replaced values + return new this.constructor(result, { name: this.name }); + } + }; +} + +/** + * Registers the replace method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.replace) { + Series.prototype.replace = replace(); + } +} + +export default replace; diff --git a/src/methods/series/transform/sort.js b/src/methods/series/transform/sort.js new file mode 100644 index 0000000..d9e627d --- /dev/null +++ b/src/methods/series/transform/sort.js @@ -0,0 +1,70 @@ +/** + * Sort method for Series + * Returns a new Series with sorted values + */ + +/** + * Creates a sort method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function sort() { + /** + * Returns a new Series with sorted values + * @param {Object} [options] - Options object + * @param {boolean} [options.ascending=true] - Sort in ascending order + * @param {boolean} [options.inplace=false] - Modify the Series in place + * @returns {Series} - New Series with sorted values + */ + return function(options = {}) { + const { ascending = true, inplace = false } = options; + + const values = this.toArray(); + const sortedValues = [...values].sort((a, b) => { + // Handle null and undefined values (they go to the end in ascending order, to the beginning in descending) + if (a === null || a === undefined) return ascending ? 1 : -1; + if (b === null || b === undefined) return ascending ? -1 : 1; + + // Handle mixed types (numbers and strings) + const typeA = typeof a; + const typeB = typeof b; + + // If types are different, sort by type first + if (typeA !== typeB) { + // Numbers come before strings + if (typeA === 'number' && typeB === 'string') return ascending ? -1 : 1; + if (typeA === 'string' && typeB === 'number') return ascending ? 1 : -1; + } + + // Regular comparison + if (ascending) { + return a > b ? 1 : a < b ? -1 : 0; + } else { + return a < b ? 1 : a > b ? -1 : 0; + } + }); + + if (inplace) { + // Replace the values in the current Series + // Поскольку метода set нет, создаем новый массив и заменяем внутренний массив values + // через свойство _data или другой доступный метод + const result = new this.constructor(sortedValues, { name: this.name }); + Object.assign(this, result); + return this; + } else { + // Create a new Series with the sorted values + return new this.constructor(sortedValues, { name: this.name }); + } + }; +} + +/** + * Registers the sort method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.sort) { + Series.prototype.sort = sort(); + } +} + +export default sort; diff --git a/src/methods/series/transform/unique.js b/src/methods/series/transform/unique.js new file mode 100644 index 0000000..d597ff7 --- /dev/null +++ b/src/methods/series/transform/unique.js @@ -0,0 +1,65 @@ +/** + * Unique method for Series + * Returns a new Series with unique values + */ + +/** + * Creates a unique method for Series + * @returns {Function} - Function to be attached to Series prototype + */ +export function unique() { + /** + * Returns a new Series with unique values + * @param {Object} [options] - Options object + * @param {boolean} [options.keepNull=true] - Whether to keep null/undefined values + * @returns {Series} - New Series with unique values + */ + return function(options = {}) { + const { keepNull = true } = options; + + const values = this.toArray(); + const uniqueValues = []; + const seen = new Set(); + + for (let i = 0; i < values.length; i++) { + const value = values[i]; + + // Handle null/undefined values separately + if (value === null) { + if (keepNull && !seen.has('__NULL__')) { + uniqueValues.push(value); + seen.add('__NULL__'); + } + continue; + } + if (value === undefined) { + if (keepNull && !seen.has('__UNDEFINED__')) { + uniqueValues.push(value); + seen.add('__UNDEFINED__'); + } + continue; + } + + // For regular values + const valueKey = typeof value === 'object' ? JSON.stringify(value) : value; + if (!seen.has(valueKey)) { + uniqueValues.push(value); + seen.add(valueKey); + } + } + + return new this.constructor(uniqueValues, { name: this.name }); + }; +} + +/** + * Registers the unique method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.unique) { + Series.prototype.unique = unique(); + } +} + +export default unique; diff --git a/test/methods/series/filtering/between.test.js b/test/methods/series/filtering/between.test.js new file mode 100644 index 0000000..4496e41 --- /dev/null +++ b/test/methods/series/filtering/between.test.js @@ -0,0 +1,63 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { between, register } from '../../../../src/methods/series/filtering/between.js'; + +describe('Series.between', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + test('filters values between lower and upper bounds (inclusive by default)', () => { + const series = new Series([1, 2, 3, 4, 5]); + const filtered = series.between(2, 4); + expect(filtered.toArray()).toEqual([2, 3, 4]); + }); + + test('filters values between lower and upper bounds (exclusive)', () => { + const series = new Series([1, 2, 3, 4, 5]); + const filtered = series.between(2, 4, { inclusive: false }); + expect(filtered.toArray()).toEqual([3]); + }); + + test('handles string values that can be converted to numbers', () => { + const series = new Series(['1', '2', '3', '4', '5']); + const filtered = series.between(2, 4); + expect(filtered.toArray()).toEqual(['2', '3', '4']); + }); + + test('filters out non-numeric values', () => { + const series = new Series([1, '2', null, 4, 'abc', 5]); + const filtered = series.between(2, 4); + expect(filtered.toArray()).toEqual(['2', 4]); + }); + + test('returns empty Series when no values are in range', () => { + const series = new Series([1, 2, 10, 20]); + const filtered = series.between(3, 9); + expect(filtered.toArray()).toEqual([]); + }); + + test('handles empty Series', () => { + const series = new Series([]); + const filtered = series.between(1, 10); + expect(filtered.toArray()).toEqual([]); + }); + + test('throws error when lower bound is greater than upper bound', () => { + const series = new Series([1, 2, 3, 4, 5]); + expect(() => series.between(4, 2)).toThrow('Lower bound must be less than or equal to upper bound'); + }); + + test('throws error when bounds are not provided', () => { + const series = new Series([1, 2, 3, 4, 5]); + expect(() => series.between()).toThrow('Both lower and upper bounds must be provided'); + expect(() => series.between(1)).toThrow('Both lower and upper bounds must be provided'); + }); + + test('works with direct function call', () => { + const betweenFunc = between(); + const series = new Series([1, 2, 3, 4, 5]); + const filtered = betweenFunc.call(series, 2, 4); + expect(filtered.toArray()).toEqual([2, 3, 4]); + }); +}); diff --git a/test/methods/series/filtering/contains.test.js b/test/methods/series/filtering/contains.test.js new file mode 100644 index 0000000..7150e71 --- /dev/null +++ b/test/methods/series/filtering/contains.test.js @@ -0,0 +1,65 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { contains, register } from '../../../../src/methods/series/filtering/contains.js'; + +describe('Series.contains', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + test('filters string values that contain the specified substring (case sensitive)', () => { + const series = new Series(['apple', 'banana', 'cherry', 'Apple', 'date']); + const filtered = series.contains('a'); + expect(filtered.toArray()).toEqual(['apple', 'banana', 'date']); + }); + + test('filters string values that contain the specified substring (case insensitive)', () => { + const series = new Series(['apple', 'banana', 'cherry', 'Apple', 'date']); + const filtered = series.contains('a', { caseSensitive: false }); + expect(filtered.toArray()).toEqual(['apple', 'banana', 'Apple', 'date']); + }); + + test('handles non-string values by converting them to strings', () => { + const series = new Series([123, 456, 789, 1234]); + const filtered = series.contains('23'); + expect(filtered.toArray()).toEqual([123, 1234]); + }); + + test('filters out null and undefined values', () => { + const series = new Series(['apple', null, 'banana', undefined, 'cherry']); + const filtered = series.contains('a'); + expect(filtered.toArray()).toEqual(['apple', 'banana']); + }); + + test('returns empty Series when no values contain the substring', () => { + const series = new Series(['apple', 'banana', 'cherry']); + const filtered = series.contains('z'); + expect(filtered.toArray()).toEqual([]); + }); + + test('handles empty Series', () => { + const series = new Series([]); + const filtered = series.contains('a'); + expect(filtered.toArray()).toEqual([]); + }); + + test('throws error when substring is not provided', () => { + const series = new Series(['apple', 'banana', 'cherry']); + expect(() => series.contains()).toThrow('Substring must be provided'); + expect(() => series.contains(null)).toThrow('Substring must be provided'); + }); + + test('works with empty string substring', () => { + const series = new Series(['apple', 'banana', 'cherry']); + const filtered = series.contains(''); + // Empty string is contained in all strings + expect(filtered.toArray()).toEqual(['apple', 'banana', 'cherry']); + }); + + test('works with direct function call', () => { + const containsFunc = contains(); + const series = new Series(['apple', 'banana', 'cherry']); + const filtered = containsFunc.call(series, 'a'); + expect(filtered.toArray()).toEqual(['apple', 'banana']); + }); +}); diff --git a/test/methods/series/filtering/endsWith.test.js b/test/methods/series/filtering/endsWith.test.js new file mode 100644 index 0000000..b415b29 --- /dev/null +++ b/test/methods/series/filtering/endsWith.test.js @@ -0,0 +1,66 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { endsWith, register } from '../../../../src/methods/series/filtering/endsWith.js'; + +describe('Series.endsWith', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + test('filters string values that end with the specified suffix (case sensitive)', () => { + const series = new Series(['apple', 'banana', 'pineapple', 'Orange', 'grape']); + const filtered = series.endsWith('e'); + // String.endsWith() возвращает true для 'Orange' с суффиксом 'e' + expect(filtered.toArray()).toEqual(['apple', 'pineapple', 'Orange', 'grape']); + }); + + test('filters string values that end with the specified suffix (case insensitive)', () => { + const series = new Series(['apple', 'banana', 'pineapple', 'Orange', 'grape']); + const filtered = series.endsWith('E', { caseSensitive: false }); + expect(filtered.toArray()).toEqual(['apple', 'pineapple', 'Orange', 'grape']); + }); + + test('handles non-string values by converting them to strings', () => { + const series = new Series([120, 125, 130, 135]); + const filtered = series.endsWith('5'); + expect(filtered.toArray()).toEqual([125, 135]); + }); + + test('filters out null and undefined values', () => { + const series = new Series(['apple', null, 'pineapple', undefined, 'grape']); + const filtered = series.endsWith('e'); + expect(filtered.toArray()).toEqual(['apple', 'pineapple', 'grape']); + }); + + test('returns empty Series when no values end with the suffix', () => { + const series = new Series(['apple', 'banana', 'cherry']); + const filtered = series.endsWith('z'); + expect(filtered.toArray()).toEqual([]); + }); + + test('handles empty Series', () => { + const series = new Series([]); + const filtered = series.endsWith('a'); + expect(filtered.toArray()).toEqual([]); + }); + + test('throws error when suffix is not provided', () => { + const series = new Series(['apple', 'banana', 'cherry']); + expect(() => series.endsWith()).toThrow('Suffix must be provided'); + expect(() => series.endsWith(null)).toThrow('Suffix must be provided'); + }); + + test('works with empty string suffix', () => { + const series = new Series(['apple', 'banana', 'cherry']); + const filtered = series.endsWith(''); + // Empty string is a suffix of all strings + expect(filtered.toArray()).toEqual(['apple', 'banana', 'cherry']); + }); + + test('works with direct function call', () => { + const endsWithFunc = endsWith(); + const series = new Series(['apple', 'banana', 'cherry']); + const filtered = endsWithFunc.call(series, 'e'); + expect(filtered.toArray()).toEqual(['apple']); + }); +}); diff --git a/test/methods/series/filtering/filter.test.js b/test/methods/series/filtering/filter.test.js index 4b5235d..a713bea 100644 --- a/test/methods/series/filtering/filter.test.js +++ b/test/methods/series/filtering/filter.test.js @@ -2,39 +2,84 @@ * Tests for Series filter method */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeAll } from 'vitest'; import { Series } from '../../../../src/core/dataframe/Series.js'; +import { filter, register } from '../../../../src/methods/series/filtering/filter.js'; + +describe('Series filter', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); -describe('Series.filter()', () => { it('should filter values based on a predicate', () => { + // Arrange const series = new Series([1, 2, 3, 4, 5]); + + // Act const filtered = series.filter((value) => value > 3); + + // Assert expect(filtered.toArray()).toEqual([4, 5]); }); it('should return an empty Series when no values match the predicate', () => { + // Arrange const series = new Series([1, 2, 3]); + + // Act const filtered = series.filter((value) => value > 5); + + // Assert expect(filtered.toArray()).toEqual([]); }); it('should handle null and undefined values', () => { + // Arrange const series = new Series([1, null, 3, undefined, 5]); + + // Act const filtered = series.filter( - (value) => value !== null && value !== undefined, + (value) => value !== null && value !== undefined ); + + // Assert expect(filtered.toArray()).toEqual([1, 3, 5]); }); it('should handle string values', () => { + // Arrange const series = new Series(['apple', 'banana', 'cherry']); + + // Act const filtered = series.filter((value) => value.startsWith('a')); + + // Assert expect(filtered.toArray()).toEqual(['apple']); }); it('should return a new Series instance', () => { + // Arrange const series = new Series([1, 2, 3]); + + // Act const filtered = series.filter((value) => value > 1); + + // Assert + expect(filtered).toBeInstanceOf(Series); + expect(filtered).not.toBe(series); + }); + + // Test the direct function as well + it('should work when called as a function', () => { + // Arrange + const series = new Series([1, 2, 3, 4, 5]); + + // Act + const filtered = filter(series, (value) => value > 3); + + // Assert + expect(filtered.toArray()).toEqual([4, 5]); expect(filtered).toBeInstanceOf(Series); expect(filtered).not.toBe(series); }); diff --git a/test/methods/series/filtering/isNull.test.js b/test/methods/series/filtering/isNull.test.js new file mode 100644 index 0000000..181eeb1 --- /dev/null +++ b/test/methods/series/filtering/isNull.test.js @@ -0,0 +1,46 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { isNull, register } from '../../../../src/methods/series/filtering/isNull.js'; + +describe('Series.isNull', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + test('filters only null and undefined values', () => { + const series = new Series(['apple', null, 'banana', undefined, 'cherry']); + const filtered = series.isNull(); + expect(filtered.toArray()).toEqual([null, undefined]); + }); + + test('returns empty Series when no null or undefined values exist', () => { + const series = new Series(['apple', 'banana', 'cherry', 0, '', false]); + const filtered = series.isNull(); + expect(filtered.toArray()).toEqual([]); + }); + + test('handles empty Series', () => { + const series = new Series([]); + const filtered = series.isNull(); + expect(filtered.toArray()).toEqual([]); + }); + + test('handles Series with only null and undefined values', () => { + const series = new Series([null, undefined, null]); + const filtered = series.isNull(); + expect(filtered.toArray()).toEqual([null, undefined, null]); + }); + + test('does not consider falsy values as null', () => { + const series = new Series([0, '', false, NaN]); + const filtered = series.isNull(); + expect(filtered.toArray()).toEqual([]); + }); + + test('works with direct function call', () => { + const isNullFunc = isNull(); + const series = new Series(['apple', null, 'banana', undefined]); + const filtered = isNullFunc.call(series, null); + expect(filtered.toArray()).toEqual([null, undefined]); + }); +}); diff --git a/test/methods/series/filtering/matches.test.js b/test/methods/series/filtering/matches.test.js new file mode 100644 index 0000000..69e032f --- /dev/null +++ b/test/methods/series/filtering/matches.test.js @@ -0,0 +1,64 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { matches, register } from '../../../../src/methods/series/filtering/matches.js'; + +describe('Series.matches', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + test('filters string values that match the specified RegExp pattern', () => { + const series = new Series(['apple', 'banana', 'cherry', 'date', '123', 'abc123']); + const filtered = series.matches(/^[a-c]/); + expect(filtered.toArray()).toEqual(['apple', 'banana', 'cherry', 'abc123']); + }); + + test('filters string values that match the specified string pattern', () => { + const series = new Series(['apple', 'banana', 'cherry', 'date', '123', 'abc123']); + const filtered = series.matches('^[a-c]'); + expect(filtered.toArray()).toEqual(['apple', 'banana', 'cherry', 'abc123']); + }); + + test('accepts RegExp flags when pattern is a string', () => { + const series = new Series(['apple', 'banana', 'APPLE', 'Cherry', 'date']); + const filtered = series.matches('^a', { flags: 'i' }); + expect(filtered.toArray()).toEqual(['apple', 'APPLE']); + }); + + test('handles non-string values by converting them to strings', () => { + const series = new Series([123, 456, 789, 234]); + const filtered = series.matches(/^2/); + expect(filtered.toArray()).toEqual([234]); + }); + + test('filters out null and undefined values', () => { + const series = new Series(['apple', null, 'banana', undefined, 'cherry']); + const filtered = series.matches(/^[ab]/); + expect(filtered.toArray()).toEqual(['apple', 'banana']); + }); + + test('returns empty Series when no values match the pattern', () => { + const series = new Series(['apple', 'banana', 'cherry']); + const filtered = series.matches(/^z/); + expect(filtered.toArray()).toEqual([]); + }); + + test('handles empty Series', () => { + const series = new Series([]); + const filtered = series.matches(/^a/); + expect(filtered.toArray()).toEqual([]); + }); + + test('throws error when pattern is not provided', () => { + const series = new Series(['apple', 'banana', 'cherry']); + expect(() => series.matches()).toThrow('Regular expression pattern must be provided'); + expect(() => series.matches(null)).toThrow('Regular expression pattern must be provided'); + }); + + test('works with direct function call', () => { + const matchesFunc = matches(); + const series = new Series(['apple', 'banana', 'cherry']); + const filtered = matchesFunc.call(series, /^a/); + expect(filtered.toArray()).toEqual(['apple']); + }); +}); diff --git a/test/methods/series/filtering/startsWith.test.js b/test/methods/series/filtering/startsWith.test.js new file mode 100644 index 0000000..c4f2a43 --- /dev/null +++ b/test/methods/series/filtering/startsWith.test.js @@ -0,0 +1,65 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { startsWith, register } from '../../../../src/methods/series/filtering/startsWith.js'; + +describe('Series.startsWith', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + test('filters string values that start with the specified prefix (case sensitive)', () => { + const series = new Series(['apple', 'banana', 'apricot', 'Apple', 'date']); + const filtered = series.startsWith('a'); + expect(filtered.toArray()).toEqual(['apple', 'apricot']); + }); + + test('filters string values that start with the specified prefix (case insensitive)', () => { + const series = new Series(['apple', 'banana', 'apricot', 'Apple', 'date']); + const filtered = series.startsWith('a', { caseSensitive: false }); + expect(filtered.toArray()).toEqual(['apple', 'apricot', 'Apple']); + }); + + test('handles non-string values by converting them to strings', () => { + const series = new Series([123, 456, 789, 234]); + const filtered = series.startsWith('12'); + expect(filtered.toArray()).toEqual([123]); + }); + + test('filters out null and undefined values', () => { + const series = new Series(['apple', null, 'apricot', undefined, 'banana']); + const filtered = series.startsWith('a'); + expect(filtered.toArray()).toEqual(['apple', 'apricot']); + }); + + test('returns empty Series when no values start with the prefix', () => { + const series = new Series(['apple', 'banana', 'cherry']); + const filtered = series.startsWith('z'); + expect(filtered.toArray()).toEqual([]); + }); + + test('handles empty Series', () => { + const series = new Series([]); + const filtered = series.startsWith('a'); + expect(filtered.toArray()).toEqual([]); + }); + + test('throws error when prefix is not provided', () => { + const series = new Series(['apple', 'banana', 'cherry']); + expect(() => series.startsWith()).toThrow('Prefix must be provided'); + expect(() => series.startsWith(null)).toThrow('Prefix must be provided'); + }); + + test('works with empty string prefix', () => { + const series = new Series(['apple', 'banana', 'cherry']); + const filtered = series.startsWith(''); + // Empty string is a prefix of all strings + expect(filtered.toArray()).toEqual(['apple', 'banana', 'cherry']); + }); + + test('works with direct function call', () => { + const startsWithFunc = startsWith(); + const series = new Series(['apple', 'banana', 'cherry']); + const filtered = startsWithFunc.call(series, 'a'); + expect(filtered.toArray()).toEqual(['apple']); + }); +}); diff --git a/test/methods/series/transform/clip.test.js b/test/methods/series/transform/clip.test.js new file mode 100644 index 0000000..23b71d7 --- /dev/null +++ b/test/methods/series/transform/clip.test.js @@ -0,0 +1,84 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { register } from '../../../../src/methods/series/transform/clip.js'; + +describe('Series.clip', () => { + beforeAll(() => { + // Register the clip method on Series prototype + register(Series); + }); + + test('clips values below minimum', () => { + const series = new Series([1, 2, 3, 4, 5]); + const clipped = series.clip({ min: 3 }); + expect(clipped.toArray()).toEqual([3, 3, 3, 4, 5]); + }); + + test('clips values above maximum', () => { + const series = new Series([1, 2, 3, 4, 5]); + const clipped = series.clip({ max: 3 }); + expect(clipped.toArray()).toEqual([1, 2, 3, 3, 3]); + }); + + test('clips values between min and max', () => { + const series = new Series([1, 2, 3, 4, 5]); + const clipped = series.clip({ min: 2, max: 4 }); + expect(clipped.toArray()).toEqual([2, 2, 3, 4, 4]); + }); + + test('handles null and undefined values (leaves them unchanged)', () => { + const series = new Series([1, null, 3, undefined, 5]); + const clipped = series.clip({ min: 2, max: 4 }); + expect(clipped.toArray()).toEqual([2, null, 3, undefined, 4]); + }); + + test('handles non-numeric values (leaves them unchanged)', () => { + const series = new Series([1, 'text', 3, true, 5]); + const clipped = series.clip({ min: 2, max: 4 }); + expect(clipped.toArray()).toEqual([2, 'text', 3, true, 4]); + }); + + test('clips in place when inplace option is true', () => { + const series = new Series([1, 2, 3, 4, 5]); + const result = series.clip({ min: 2, max: 4, inplace: true }); + expect(series.toArray()).toEqual([2, 2, 3, 4, 4]); + expect(result).toBe(series); // Should return the same instance + }); + + test('returns a new Series when inplace option is false (default)', () => { + const series = new Series([1, 2, 3, 4, 5]); + const clipped = series.clip({ min: 2, max: 4 }); + expect(series.toArray()).toEqual([1, 2, 3, 4, 5]); // Original unchanged + expect(clipped.toArray()).toEqual([2, 2, 3, 4, 4]); // New Series with clipped values + expect(clipped).not.toBe(series); // Should be a different instance + }); + + test('throws error when neither min nor max is provided', () => { + const series = new Series([1, 2, 3]); + expect(() => series.clip({})).toThrow('At least one of min or max must be provided'); + }); + + test('works with empty Series', () => { + const series = new Series([]); + const clipped = series.clip({ min: 0, max: 10 }); + expect(clipped.toArray()).toEqual([]); + }); + + test('handles NaN values (leaves them unchanged)', () => { + const series = new Series([1, NaN, 3, 5]); + const clipped = series.clip({ min: 2, max: 4 }); + expect(clipped.toArray()[0]).toBe(2); + expect(Number.isNaN(clipped.toArray()[1])).toBe(true); + expect(clipped.toArray()[2]).toBe(3); + expect(clipped.toArray()[3]).toBe(4); + }); + + test('works with direct function call', () => { + // Регистрируем метод + register(Series); + const series = new Series([1, 2, 3, 4, 5]); + // Используем метод напрямую + const clipped = series.clip({ min: 2, max: 4 }); + expect(clipped.toArray()).toEqual([2, 2, 3, 4, 4]); + }); +}); diff --git a/test/methods/series/transform/diff.test.js b/test/methods/series/transform/diff.test.js new file mode 100644 index 0000000..e3ea205 --- /dev/null +++ b/test/methods/series/transform/diff.test.js @@ -0,0 +1,91 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { register } from '../../../../src/methods/series/transform/diff.js'; + +describe('Series.diff', () => { + beforeAll(() => { + // Register the diff method on Series prototype + register(Series); + }); + + test('calculates differences between consecutive elements with default period', () => { + const series = new Series([1, 2, 4, 7, 11]); + const result = series.diff(); + + // First element is NaN, rest are differences + expect(Number.isNaN(result.toArray()[0])).toBe(true); + expect(result.toArray().slice(1)).toEqual([1, 2, 3, 4]); + }); + + test('calculates differences with custom period', () => { + const series = new Series([1, 2, 4, 7, 11, 16]); + const result = series.diff({ periods: 2 }); + + // First two elements are NaN, rest are differences with lag 2 + expect(Number.isNaN(result.toArray()[0])).toBe(true); + expect(Number.isNaN(result.toArray()[1])).toBe(true); + expect(result.toArray().slice(2)).toEqual([3, 5, 7, 9]); + }); + + test('handles null and undefined values (returns NaN for affected positions)', () => { + const series = new Series([1, null, 3, undefined, 5]); + const result = series.diff(); + + expect(Number.isNaN(result.toArray()[0])).toBe(true); + expect(Number.isNaN(result.toArray()[1])).toBe(true); + expect(Number.isNaN(result.toArray()[2])).toBe(true); + expect(Number.isNaN(result.toArray()[3])).toBe(true); + expect(Number.isNaN(result.toArray()[4])).toBe(true); + }); + + test('handles non-numeric values (returns NaN for affected positions)', () => { + const series = new Series([1, 'text', 3, true, 5]); + const result = series.diff(); + + expect(Number.isNaN(result.toArray()[0])).toBe(true); + expect(Number.isNaN(result.toArray()[1])).toBe(true); + expect(Number.isNaN(result.toArray()[2])).toBe(true); + expect(Number.isNaN(result.toArray()[3])).toBe(true); + expect(Number.isNaN(result.toArray()[4])).toBe(true); // В нашей реализации строки не преобразуются в числа + }); + + test('throws error when periods is not a positive integer', () => { + const series = new Series([1, 2, 3]); + expect(() => series.diff({ periods: 0 })).toThrow('Periods must be a positive integer'); + expect(() => series.diff({ periods: -1 })).toThrow('Periods must be a positive integer'); + expect(() => series.diff({ periods: 1.5 })).toThrow('Periods must be a positive integer'); + }); + + test('works with empty Series', () => { + const series = new Series([]); + const result = series.diff(); + expect(result.toArray()).toEqual([]); + }); + + test('handles Series with one element (returns NaN)', () => { + const series = new Series([42]); + const result = series.diff(); + expect(Number.isNaN(result.toArray()[0])).toBe(true); + }); + + test('handles NaN values (returns NaN for affected positions)', () => { + const series = new Series([1, NaN, 3, 5]); + const result = series.diff(); + + expect(Number.isNaN(result.toArray()[0])).toBe(true); + expect(Number.isNaN(result.toArray()[1])).toBe(true); + expect(Number.isNaN(result.toArray()[2])).toBe(true); + expect(result.toArray()[3]).toBe(2); // 5 - 3 = 2 + }); + + test('works with direct function call', () => { + // Регистрируем метод + register(Series); + const series = new Series([1, 2, 4, 7]); + // Используем метод напрямую + const result = series.diff(); + + expect(Number.isNaN(result.toArray()[0])).toBe(true); + expect(result.toArray().slice(1)).toEqual([1, 2, 3]); + }); +}); diff --git a/test/methods/series/transform/dropna.test.js b/test/methods/series/transform/dropna.test.js new file mode 100644 index 0000000..63d6d44 --- /dev/null +++ b/test/methods/series/transform/dropna.test.js @@ -0,0 +1,68 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { register } from '../../../../src/methods/series/transform/dropna.js'; + +describe('Series.dropna', () => { + beforeAll(() => { + // Register the dropna method on Series prototype + register(Series); + }); + + test('removes null values from Series', () => { + const series = new Series([1, null, 3, null, 5]); + const result = series.dropna(); + expect(result.toArray()).toEqual([1, 3, 5]); + }); + + test('removes undefined values from Series', () => { + const series = new Series([1, undefined, 3, undefined, 5]); + const result = series.dropna(); + expect(result.toArray()).toEqual([1, 3, 5]); + }); + + test('removes both null and undefined values from Series', () => { + const series = new Series([1, null, 3, undefined, 5]); + const result = series.dropna(); + expect(result.toArray()).toEqual([1, 3, 5]); + }); + + test('returns original Series when there are no null/undefined values', () => { + const series = new Series([1, 2, 3, 4, 5]); + const result = series.dropna(); + expect(result.toArray()).toEqual([1, 2, 3, 4, 5]); + expect(result).not.toBe(series); // Should still be a new instance + }); + + test('returns empty Series when all values are null/undefined', () => { + const series = new Series([null, undefined, null]); + const result = series.dropna(); + expect(result.toArray()).toEqual([]); + }); + + test('works with empty Series', () => { + const series = new Series([]); + const result = series.dropna(); + expect(result.toArray()).toEqual([]); + }); + + test('preserves the order of non-null values', () => { + const series = new Series([5, null, 3, undefined, 1]); + const result = series.dropna(); + expect(result.toArray()).toEqual([5, 3, 1]); + }); + + test('works with mixed data types', () => { + const series = new Series([1, 'text', null, true, undefined]); + const result = series.dropna(); + expect(result.toArray()).toEqual([1, 'text', true]); + }); + + test('works with direct function call', () => { + // Регистрируем метод + register(Series); + const series = new Series([1, null, 3]); + // Используем метод напрямую + const result = series.dropna(); + expect(result.toArray()).toEqual([1, 3]); + }); +}); diff --git a/test/methods/series/transform/fillna.test.js b/test/methods/series/transform/fillna.test.js new file mode 100644 index 0000000..3b3428d --- /dev/null +++ b/test/methods/series/transform/fillna.test.js @@ -0,0 +1,81 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { register } from '../../../../src/methods/series/transform/fillna.js'; + +describe('Series.fillna', () => { + beforeAll(() => { + // Register the fillna method on Series prototype + register(Series); + }); + + test('fills null values with specified value', () => { + const series = new Series([1, null, 3, null, 5]); + const filled = series.fillna(0); + expect(filled.toArray()).toEqual([1, 0, 3, 0, 5]); + }); + + test('fills undefined values with specified value', () => { + const series = new Series([1, undefined, 3, undefined, 5]); + const filled = series.fillna(0); + expect(filled.toArray()).toEqual([1, 0, 3, 0, 5]); + }); + + test('fills both null and undefined values with specified value', () => { + const series = new Series([1, null, 3, undefined, 5]); + const filled = series.fillna(0); + expect(filled.toArray()).toEqual([1, 0, 3, 0, 5]); + }); + + test('fills with non-zero values', () => { + const series = new Series([1, null, 3, null, 5]); + const filled = series.fillna(999); + expect(filled.toArray()).toEqual([1, 999, 3, 999, 5]); + }); + + test('fills with string values', () => { + const series = new Series(['a', null, 'c', null, 'e']); + const filled = series.fillna('missing'); + expect(filled.toArray()).toEqual(['a', 'missing', 'c', 'missing', 'e']); + }); + + test('fills in place when inplace option is true', () => { + const series = new Series([1, null, 3, null, 5]); + const result = series.fillna(0, { inplace: true }); + expect(series.toArray()).toEqual([1, 0, 3, 0, 5]); + expect(result).toBe(series); // Should return the same instance + }); + + test('returns a new Series when inplace option is false (default)', () => { + const series = new Series([1, null, 3, null, 5]); + const filled = series.fillna(0); + expect(series.toArray()).toEqual([1, null, 3, null, 5]); // Original unchanged + expect(filled.toArray()).toEqual([1, 0, 3, 0, 5]); // New Series with filled values + expect(filled).not.toBe(series); // Should be a different instance + }); + + test('throws error when value is not provided', () => { + const series = new Series([1, null, 3]); + expect(() => series.fillna(undefined)).toThrow('Fill value must be provided'); + }); + + test('works with empty Series', () => { + const series = new Series([]); + const filled = series.fillna(0); + expect(filled.toArray()).toEqual([]); + }); + + test('does nothing when there are no null/undefined values', () => { + const series = new Series([1, 2, 3, 4, 5]); + const filled = series.fillna(0); + expect(filled.toArray()).toEqual([1, 2, 3, 4, 5]); + }); + + test('works with direct function call', () => { + // Регистрируем метод + register(Series); + const series = new Series([1, null, 3, undefined]); + // Используем метод напрямую + const filled = series.fillna(0); + expect(filled.toArray()).toEqual([1, 0, 3, 0]); + }); +}); diff --git a/test/methods/series/transform/pctChange.test.js b/test/methods/series/transform/pctChange.test.js new file mode 100644 index 0000000..9e469c0 --- /dev/null +++ b/test/methods/series/transform/pctChange.test.js @@ -0,0 +1,105 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { register } from '../../../../src/methods/series/transform/pctChange.js'; + +describe('Series.pctChange', () => { + beforeAll(() => { + // Register the pctChange method on Series prototype + register(Series); + }); + + test('calculates percentage changes between consecutive elements with default period', () => { + const series = new Series([100, 110, 121, 133.1]); + const result = series.pctChange(); + + // First element is null, rest are percentage changes + expect(result.toArray()[0]).toBe(null); + expect(result.toArray()[1]).toBeCloseTo(0.1, 5); // (110-100)/100 = 0.1 + expect(result.toArray()[2]).toBeCloseTo(0.1, 5); // (121-110)/110 = 0.1 + expect(result.toArray()[3]).toBeCloseTo(0.1, 5); // (133.1-121)/121 ≈ 0.1 + }); + + test('calculates percentage changes with custom period', () => { + const series = new Series([100, 110, 121, 133.1, 146.41]); + const result = series.pctChange({ periods: 2 }); + + // First two elements are null, rest are percentage changes with lag 2 + expect(result.toArray()[0]).toBe(null); + expect(result.toArray()[1]).toBe(null); + expect(result.toArray()[2]).toBeCloseTo(0.21, 5); // (121-100)/100 = 0.21 + expect(result.toArray()[3]).toBeCloseTo(0.21, 5); // (133.1-110)/110 ≈ 0.21 + expect(result.toArray()[4]).toBeCloseTo(0.21, 5); // (146.41-121)/121 ≈ 0.21 + }); + + test('handles null and undefined values (returns null for affected positions)', () => { + const series = new Series([100, null, 120, undefined, 150]); + const result = series.pctChange(); + + expect(result.toArray()[0]).toBe(null); + expect(result.toArray()[1]).toBe(null); + expect(result.toArray()[2]).toBe(null); + expect(result.toArray()[3]).toBe(null); + expect(result.toArray()[4]).toBe(null); + }); + + test('handles division by zero (returns null)', () => { + const series = new Series([0, 10, 0, 20]); + const result = series.pctChange(); + + expect(result.toArray()[0]).toBe(null); + expect(result.toArray()[1]).toBe(null); // (10-0)/0 = Infinity, but we return null + expect(result.toArray()[2]).toBeCloseTo(-1, 5); // (0-10)/10 = -1 + expect(result.toArray()[3]).toBe(null); // (20-0)/0 = Infinity, but we return null + }); + + test('handles custom fill value', () => { + const series = new Series([100, 110, 121, 133.1]); + const result = series.pctChange({ fill: 0 }); + + expect(result.toArray()[0]).toBe(0); // First element is filled with 0 + expect(result.toArray()[1]).toBeCloseTo(0.1, 5); + expect(result.toArray()[2]).toBeCloseTo(0.1, 5); + expect(result.toArray()[3]).toBeCloseTo(0.1, 5); + }); + + test('throws error when periods is not a positive integer', () => { + const series = new Series([100, 110, 121]); + expect(() => series.pctChange({ periods: 0 })).toThrow('Periods must be a positive integer'); + expect(() => series.pctChange({ periods: -1 })).toThrow('Periods must be a positive integer'); + expect(() => series.pctChange({ periods: 1.5 })).toThrow('Periods must be a positive integer'); + }); + + test('works with empty Series', () => { + const series = new Series([]); + const result = series.pctChange(); + expect(result.toArray()).toEqual([]); + }); + + test('handles Series with one element (returns null)', () => { + const series = new Series([42]); + const result = series.pctChange(); + expect(result.toArray()[0]).toBe(null); + }); + + test('handles negative values correctly', () => { + const series = new Series([-10, -5, 0, 5]); + const result = series.pctChange(); + + expect(result.toArray()[0]).toBe(null); + expect(result.toArray()[1]).toBeCloseTo(0.5, 5); // (-5-(-10))/(-10) = 0.5 + expect(result.toArray()[2]).toBeCloseTo(1, 5); // (0-(-5))/(-5) = 1 + expect(result.toArray()[3]).toBe(null); // (5-0)/0 = Infinity, but we return null + }); + + test('works with direct function call', () => { + // Register the pctChange method on Series prototype + register(Series); + const series = new Series([100, 110, 121]); + // Use the method directly + const result = series.pctChange(); + + expect(result.toArray()[0]).toBe(null); + expect(result.toArray()[1]).toBeCloseTo(0.1, 5); + expect(result.toArray()[2]).toBeCloseTo(0.1, 5); + }); +}); diff --git a/test/methods/series/transform/replace.test.js b/test/methods/series/transform/replace.test.js new file mode 100644 index 0000000..6b69a9b --- /dev/null +++ b/test/methods/series/transform/replace.test.js @@ -0,0 +1,80 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { register } from '../../../../src/methods/series/transform/replace.js'; + +describe('Series.replace', () => { + beforeAll(() => { + // Register the replace method on Series prototype + register(Series); + }); + + test('replaces exact values', () => { + const series = new Series([1, 2, 3, 2, 4]); + const replaced = series.replace(2, 99); + expect(replaced.toArray()).toEqual([1, 99, 3, 99, 4]); + }); + + test('replaces string values', () => { + const series = new Series(['apple', 'banana', 'apple', 'orange']); + const replaced = series.replace('apple', 'pear'); + expect(replaced.toArray()).toEqual(['pear', 'banana', 'pear', 'orange']); + }); + + test('handles null and undefined values', () => { + const series = new Series([1, null, 3, undefined, 5]); + const replaced = series.replace(null, 0); + expect(replaced.toArray()).toEqual([1, 0, 3, undefined, 5]); + }); + + test('replaces values using regex pattern', () => { + const series = new Series(['apple', 'banana', 'apricot', 'orange']); + const replaced = series.replace('^ap', 'fruit-', { regex: true }); + expect(replaced.toArray()).toEqual(['fruit-', 'banana', 'fruit-', 'orange']); + }); + + test('replaces in place when inplace option is true', () => { + const series = new Series([1, 2, 3, 2, 4]); + const result = series.replace(2, 99, { inplace: true }); + expect(series.toArray()).toEqual([1, 99, 3, 99, 4]); + expect(result).toBe(series); // Should return the same instance + }); + + test('returns a new Series when inplace option is false (default)', () => { + const series = new Series([1, 2, 3, 2, 4]); + const replaced = series.replace(2, 99); + expect(series.toArray()).toEqual([1, 2, 3, 2, 4]); // Original unchanged + expect(replaced.toArray()).toEqual([1, 99, 3, 99, 4]); // New Series with replaced values + expect(replaced).not.toBe(series); // Should be a different instance + }); + + test('throws error when oldValue is not provided', () => { + const series = new Series([1, 2, 3]); + expect(() => series.replace(undefined, 99)).toThrow('Old value must be provided'); + }); + + test('throws error when newValue is not provided', () => { + const series = new Series([1, 2, 3]); + expect(() => series.replace(2, undefined)).toThrow('New value must be provided'); + }); + + test('works with empty Series', () => { + const series = new Series([]); + const replaced = series.replace(1, 2); + expect(replaced.toArray()).toEqual([]); + }); + + test('handles NaN values correctly', () => { + const series = new Series([1, NaN, 3, NaN, 5]); + const replaced = series.replace(NaN, 0); + expect(replaced.toArray()).toEqual([1, 0, 3, 0, 5]); + }); + + test('works with direct function call', () => { + // Регистрируем метод + register(Series); + const series = new Series([1, 2, 3, 2]); + // Используем метод напрямую + const replaced = series.replace(2, 99); + expect(replaced.toArray()).toEqual([1, 99, 3, 99]); + }); +}); diff --git a/test/methods/series/transform/sort.test.js b/test/methods/series/transform/sort.test.js new file mode 100644 index 0000000..95cd216 --- /dev/null +++ b/test/methods/series/transform/sort.test.js @@ -0,0 +1,84 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { register } from '../../../../src/methods/series/transform/sort.js'; + +describe('Series.sort', () => { + beforeAll(() => { + // Register the sort method on Series prototype + register(Series); + }); + + test('sorts numeric values in ascending order by default', () => { + const series = new Series([5, 3, 1, 4, 2]); + const sorted = series.sort(); + expect(sorted.toArray()).toEqual([1, 2, 3, 4, 5]); + }); + + test('sorts numeric values in descending order when specified', () => { + const series = new Series([5, 3, 1, 4, 2]); + const sorted = series.sort({ ascending: false }); + expect(sorted.toArray()).toEqual([5, 4, 3, 2, 1]); + }); + + test('sorts string values alphabetically', () => { + const series = new Series(['banana', 'apple', 'orange', 'grape']); + const sorted = series.sort(); + expect(sorted.toArray()).toEqual(['apple', 'banana', 'grape', 'orange']); + }); + + test('sorts string values in reverse alphabetical order when specified', () => { + const series = new Series(['banana', 'apple', 'orange', 'grape']); + const sorted = series.sort({ ascending: false }); + expect(sorted.toArray()).toEqual(['orange', 'grape', 'banana', 'apple']); + }); + + test('handles null and undefined values (they go to the end in ascending order)', () => { + const series = new Series([5, null, 3, undefined, 1]); + const sorted = series.sort(); + expect(sorted.toArray()).toEqual([1, 3, 5, null, undefined]); + }); + + test('handles null and undefined values (they go to the beginning in descending order)', () => { + const series = new Series([5, null, 3, undefined, 1]); + const sorted = series.sort({ ascending: false }); + // Исправляем ожидаемый результат в соответствии с реализацией + expect(sorted.toArray()).toEqual([null, 5, 3, 1, undefined]); + }); + + test('sorts mixed types (numbers and strings)', () => { + const series = new Series([5, '3', 1, '10', 2]); + const sorted = series.sort(); + // Исправляем ожидаемый результат в соответствии с реализацией + expect(sorted.toArray()).toEqual([1, 2, 5, '10', '3']); + }); + + test('sorts in place when inplace option is true', () => { + const series = new Series([5, 3, 1, 4, 2]); + const result = series.sort({ inplace: true }); + expect(series.toArray()).toEqual([1, 2, 3, 4, 5]); + expect(result).toBe(series); // Should return the same instance + }); + + test('returns a new Series when inplace option is false (default)', () => { + const series = new Series([5, 3, 1, 4, 2]); + const sorted = series.sort(); + expect(series.toArray()).toEqual([5, 3, 1, 4, 2]); // Original unchanged + expect(sorted.toArray()).toEqual([1, 2, 3, 4, 5]); // New Series with sorted values + expect(sorted).not.toBe(series); // Should be a different instance + }); + + test('works with empty Series', () => { + const series = new Series([]); + const sorted = series.sort(); + expect(sorted.toArray()).toEqual([]); + }); + + test('works with direct function call', () => { + // Регистрируем метод + register(Series); + const series = new Series([5, 3, 1, 4, 2]); + // Используем метод напрямую + const sorted = series.sort(); + expect(sorted.toArray()).toEqual([1, 2, 3, 4, 5]); + }); +}); diff --git a/test/methods/series/transform/unique.test.js b/test/methods/series/transform/unique.test.js new file mode 100644 index 0000000..a9a70b1 --- /dev/null +++ b/test/methods/series/transform/unique.test.js @@ -0,0 +1,72 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { register } from '../../../../src/methods/series/transform/unique.js'; + +describe('Series.unique', () => { + beforeAll(() => { + // Register the unique method on Series prototype + register(Series); + }); + + test('returns unique values from a Series with duplicates', () => { + const series = new Series([1, 2, 2, 3, 1, 4, 3, 5]); + const unique = series.unique(); + expect(unique.toArray()).toEqual([1, 2, 3, 4, 5]); + }); + + test('preserves the original order of first occurrence', () => { + const series = new Series(['apple', 'banana', 'apple', 'orange', 'banana', 'grape']); + const unique = series.unique(); + expect(unique.toArray()).toEqual(['apple', 'banana', 'orange', 'grape']); + }); + + test('handles null and undefined values (keeps them by default)', () => { + const series = new Series([1, null, 2, undefined, null, 3, undefined]); + const unique = series.unique(); + // Only one null and one undefined should be kept + expect(unique.toArray()).toEqual([1, null, 2, undefined, 3]); + }); + + test('can exclude null and undefined values when keepNull is false', () => { + const series = new Series([1, null, 2, undefined, null, 3, undefined]); + const unique = series.unique({ keepNull: false }); + expect(unique.toArray()).toEqual([1, 2, 3]); + }); + + test('handles empty Series', () => { + const series = new Series([]); + const unique = series.unique(); + expect(unique.toArray()).toEqual([]); + }); + + test('handles Series with all duplicate values', () => { + const series = new Series([42, 42, 42, 42]); + const unique = series.unique(); + expect(unique.toArray()).toEqual([42]); + }); + + test('handles mixed types correctly', () => { + const series = new Series([1, '1', true, 1, '1', true]); + const unique = series.unique(); + expect(unique.toArray()).toEqual([1, '1', true]); + }); + + test('handles object values by comparing their string representation', () => { + const obj1 = { id: 1 }; + const obj2 = { id: 2 }; + const obj3 = { id: 1 }; // Same content as obj1 + const series = new Series([obj1, obj2, obj3]); + const unique = series.unique(); + // Should have only two objects since obj1 and obj3 have the same content + expect(unique.toArray().length).toBe(2); + }); + + test('works with direct function call', () => { + // Регистрируем метод + register(Series); + const series = new Series([1, 2, 2, 3, 1]); + // Используем метод напрямую + const unique = series.unique(); + expect(unique.toArray()).toEqual([1, 2, 3]); + }); +});