diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f13fcb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc +/release +/lib +/docs +/md-docs + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db + +package-lock.json +*.tgz diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..9eee4bf --- /dev/null +++ b/.npmignore @@ -0,0 +1,7 @@ +/src +/node_modules +.gitignore +rollup.config.js +jest.config.js +*.tgz +tsconfig.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ce5915 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# dataPipe + +dataPipe is data transformation and analytical library inspired by LINQ (C#) and Pandas - (Python) + +```js +const data = [ + { name: "John", country: "US"}, { name: "Joe", country: "US"}, { name: "Bill", country: "US"}, { name: "Adam", country: "UK"}, + { name: "Scott", country: "UK"}, { name: "Diana",country: "UK"}, { name: "Marry",country: "FR"}, { name: "Luc",country: "FR"} +] + +const summaryForUS = dataPipe(data) + .groupBy(i => i.country) + .select(g => + r = {} + r.country = dataPipe(g).first().country + r.names = dataPipe(g).map(r => r.name).join(", ") + r.count = dataPipe(g).count() + r + ) + .where(r => r.country != "US") + .toArray() +``` + +## methods (Pretty much WIP. Do not use it until v0.1) + - select / map + - filter / where + - dropColumns([]) + + - orderBy() + - thenBy() + - orderByDescending() + - thenByDescending() + + - groupBy(keySelector) + - join(array2, keySelector1, keySelector2, resultProjector) + - join(separator) - string style concatenation + - intercept() + - except() + - pivot() + - merge() + - union / concat() + + - avg / average (predicate) + - max / maximum (predicate) + - min / minimum (predicate) + - count(predicate) + - first(predicate) + - last(predicate) + + - toArray() + - toMap(keySelector, valueSelector) + - toObject(nameSelector, valueSelector) + - toCsv() + - toTsv() + + + diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..f498143 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,192 @@ +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // Respect "browser" field in package.json when resolving modules + // browser: false, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/tmp/jest_rs", + + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: null, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: null, + + // A path to a custom dependency extractor + // dependencyExtractor: null, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: null, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: null, + + // A set of global variables that need to be available in all test environments + globals: { + 'ts-jest': { + // diagnostics: false + } + }, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "json", + // "jsx", + // "ts", + // "tsx", + // "node" + // ], + + // A map from regular expressions to module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + preset: 'ts-jest', + + // Run tests from one or more projects + // projects: null, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state between every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: null, + + // Automatically restore mock state between every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: null, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + setupFiles: ['./setupJest.js'], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + // testEnvironment: "jest-environment-jsdom", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + testRegex: ['\\.spec\\.ts$'], + + // This option allows the use of a custom results processor + // testResultsProcessor: null, + + // This option allows use of a custom test runner + // testRunner: "jasmine2", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + // transform: null, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: null, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..b11e341 --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "datapipe-js", + "version": "0.0.1", + "description": "dataPipe is data transformation and analytical library inspired by LINQ (C#) and Pandas - (Python)", + "main": "lib/data-pipe.umd.js", + "module": "lib/data-pipe.esm.js", + "typings": "lib/index.d.ts", + "files": [ + "lib" + ], + "scripts": { + "test": "jest", + "test:dev": "node --inspect-brk node_modules/.bin/jest --runInBand --watch", + "build": "npx rollup -c && npm pack && npm run docs", + "docs": "npx typedoc src --plugin none", + "docs:md": "npx typedoc src --out md-docs --plugin typedoc-plugin-markdown" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/falconsoft/dataPipe.git" + }, + "author": "Pavlo Paska - ppaska@falconsoft-ltd.com", + "license": "MIT", + "bugs": { + "url": "https://github.com/falconsoft/dataPipe/issues" + }, + "homepage": "https://github.com/falconsoft/dataPipe#readme", + "keywords": [ + "data", + "data-analysis", + "linq", + "pandas", + "data-management", + "data-science", + "data-manipulation", + "json", + "data-munging", + "data-cleaning", + "data-clensing" + ], + "devDependencies": { + "@types/jest": "^24.0.23", + "jest": "^24.9.0", + "jest-fetch-mock": "^2.1.2", + "rollup": "^1.27.0", + "rollup-plugin-typescript2": "^0.25.2", + "rollup-plugin-uglify": "^6.0.3", + "ts-jest": "^24.1.0", + "typedoc": "^0.15.2", + "typedoc-plugin-markdown": "^2.2.11", + "typescript": "^3.7.2" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..c2ff146 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,28 @@ +import typescript from 'rollup-plugin-typescript2'; +import { uglify } from 'rollup-plugin-uglify'; + +const pkg = require('./package.json'); +const input = 'src/index.ts'; + +export default [{ + input, + output: [ + { file: pkg.main, name: 'P', format: 'umd', sourcemap: true, compact: true }, + ], + treeshake: true, + plugins: [ + typescript({ + clean: true + }), + uglify() + ] +}, { + input, + output: { file: pkg.module, format: 'esm', sourcemap: true, compact: true }, + treeshake: true, + plugins: [ + typescript({ + clean: true + }) + ] +}]; diff --git a/setupJest.js b/setupJest.js new file mode 100644 index 0000000..c158ca4 --- /dev/null +++ b/setupJest.js @@ -0,0 +1 @@ +global.fetch = require('jest-fetch-mock'); diff --git a/src/array.spec.ts b/src/array.spec.ts new file mode 100644 index 0000000..7197830 --- /dev/null +++ b/src/array.spec.ts @@ -0,0 +1,126 @@ +import * as pipeFuncs from './array'; + +describe('Test array methods', () => { + + const testNumberArray = [2, 6, 3, 7, 11, 7, -1]; + const testNumberArraySum = 35; + const testNumberArrayAvg = 5; + + const testAnyPrimitiveArray = ['5', 2, '33', false, true, true, true]; + const testAnyPrimitiveArraySum = 43; + const testAnyPrimitiveArrayAvg = 6.14; + const testAnyPrimitiveArrayMin = 0; + const testAnyPrimitiveArrayMax = 33; + + const testObjArray = testNumberArray.map(value => ({value})); + + const data = [{ + name: 'John', + country: 'US' + }, { + name: 'Joe', + country: 'US' + }, { + name: 'Bill', + country: 'US' + }, { + name: 'Adam', + country: 'UK' + }, { + name: 'Scott', + country: 'UK' + }, { + name: 'Diana', + country: 'UK' + }, { + name: 'Marry', + country: 'FR' + }, { + name: 'Luc', + country: 'FR' + }]; + + it('count', () => { + expect(pipeFuncs.count(testNumberArray)).toBe(testNumberArray.length); + expect(pipeFuncs.count(testAnyPrimitiveArray)).toBe(testAnyPrimitiveArray.length); + expect(pipeFuncs.count(testObjArray)).toBe(testObjArray.length); + expect(pipeFuncs.count(data, i => i.country === 'US')).toBe(3); + }) + + it('sum', () => { + expect(pipeFuncs.sum(testNumberArray)).toBe(testNumberArraySum); + expect(pipeFuncs.sum(testAnyPrimitiveArray)).toBe(testAnyPrimitiveArraySum); + expect(pipeFuncs.sum(testObjArray, obj => obj.value)).toBe(testNumberArraySum); + }) + + it('avg', () => { + expect(pipeFuncs.avg(testNumberArray)).toBe(testNumberArrayAvg); + expect(pipeFuncs.avg(testObjArray, obj => obj.value)).toBe(testNumberArrayAvg); + + const avg = pipeFuncs.avg(testAnyPrimitiveArray); + if (avg !== undefined) { + expect(Math.round(avg * 100) / 100).toBe(testAnyPrimitiveArrayAvg); + } else { + throw Error('testAnyPrimitiveArray failed'); + } + }) + + it('min', () => { + expect(pipeFuncs.min(testNumberArray)).toBe(Math.min(...testNumberArray)); + expect(pipeFuncs.min(testAnyPrimitiveArray)).toBe(testAnyPrimitiveArrayMin); + expect(pipeFuncs.min(testObjArray, obj => obj.value)).toBe(Math.min(...testNumberArray)); + }) + + it('max', () => { + expect(pipeFuncs.max(testNumberArray)).toBe(Math.max(...testNumberArray)); + expect(pipeFuncs.max(testAnyPrimitiveArray)).toBe(testAnyPrimitiveArrayMax); + expect(pipeFuncs.max(testObjArray, obj => obj.value)).toBe(Math.max(...testNumberArray)); + }) + + it('first', () => { + expect(pipeFuncs.first(testNumberArray)).toBe(testNumberArray[0]); + expect(pipeFuncs.first(testNumberArray, v => v > 6)).toBe(7); + }) + + it('last', () => { + const last = pipeFuncs.last(data, item => item.country === 'UK'); + if (last) { + expect(last.name).toBe('Diana'); + } else { + throw Error('Not found'); + } + }) + + it('groupBy', () => { + const groups = pipeFuncs.groupBy(data, item => item.country); + expect(groups.length).toBe(3); + }) + + it('flatten', () => { + const testArray = [1, 4, [2, [5, 5, [9, 7]], 11], 0, [], []]; + const flatten = pipeFuncs.flatten(testArray); + expect(flatten.length).toBe(9); + + const testArray2 = [testArray, [data], [], [testAnyPrimitiveArray]]; + const flatten2 = pipeFuncs.flatten(testArray2); + expect(flatten2.length).toBe(9 + data.length + testAnyPrimitiveArray.length); + }) + + it('countBy', () => { + const countriesCount = pipeFuncs.countBy(data, i => i.country); + expect(countriesCount['US']).toBe(3); + expect(countriesCount['UK']).toBe(3); + expect(countriesCount['FR']).toBe(2); + }); + + it('joinArray', () => { + const countries = [{code: 'US', capital: 'Washington'}, {code: 'UK', capital: 'London'}]; + const joinedArray = pipeFuncs.joinArray(data, countries, i => i.country, i2 => i2.code); + expect(joinedArray.length).toBe(8); + const item = pipeFuncs.first(joinedArray, i => i.country === 'US'); + expect(item.country).toBe('US'); + expect(item.code).toBe('US'); + expect(item.capital).toBe('Washington'); + expect(item.name).toBeTruthy(); + }) +}) diff --git a/src/array.ts b/src/array.ts new file mode 100644 index 0000000..66ea098 --- /dev/null +++ b/src/array.ts @@ -0,0 +1,215 @@ +import { Selector, Predicate } from "./models"; + +/** + * Sum of items in array. + * @param array The array to process. + * @param elementSelector Function invoked per iteration. + * @returns Sum of array. + * @public + * @example + * sum([1, 2, 5]); // 8 + * + * sum([{ val: 1 }, { val: 5 }], i => i.val); // 6 + */ +export function sum(array: any[], elementSelector?: Selector): number | undefined { + if (!Array.isArray(array)) return; + const sum: number = (array.reduce((prev: number, val) => { + const numberVal = getNumberValue(val, elementSelector); + if (numberVal !== undefined) { + prev += numberVal; + } + return prev; + }, 0) as number); + return sum; +} + +/** + * Average of array items. + * @param array The array to process. + * @param elementSelector Function invoked per iteration. + * @public + * @example + * avg([1, 5, 3]); // 3 + */ +export function avg(array: any[], elementSelector?: Selector): number | undefined { + const s = sum(array, elementSelector); + if (s) { + return s / array.length; + } +} + +/** + * Count of elements in array. + * @public + * @param array The array to process. + * @param predicate Predicate function invoked per iteration. + */ +export function count(array: any[], predicate?: Predicate): number | undefined { + if (!Array.isArray(array)) return; + if (!predicate || typeof predicate !== 'function') { + return array.length; + } + return array.filter(predicate).length; +} + +/** + * Computes the minimum value of array. + * @param array The array to process. + * @param elementSelector Function invoked per iteration. + */ +export function min(array: any[], elementSelector?: Selector): number | undefined { + if (!Array.isArray(array)) return; + return Math.min(...getNumberValuesArray(array, elementSelector)); +} + +/** + * Computes the maximum value of array. + * @public + * @param array The array to process. + * @param elementSelector Function invoked per iteration. + */ +export function max(array: any[], elementSelector?: Selector): number | null { + if (!Array.isArray(array)) return null; + return Math.max(...getNumberValuesArray(array, elementSelector)); +} + +/** + * Gets first item in array satisfies predicate. + * @param array The array to process. + * @param predicate Predicate function invoked per iteration. + */ +export function first(array: T[], predicate?: Predicate): T | undefined { + if (!Array.isArray(array) || !array.length) return; + if (!predicate) { + return array[0]; + } + for (let i = 0; i < array.length; i++) { + if (predicate(array[i])) { + return array[i]; + } + } +} + +/** + * Gets last item in array satisfies predicate. + * @param array The array to process. + * @param predicate Predicate function invoked per iteration. + */ +export function last(array: T[], predicate?: Predicate): T | undefined { + if (!Array.isArray(array) || !array.length) return; + let lastIndex = array.length - 1; + if (!predicate) { + return array[lastIndex]; + } + + for (; lastIndex >= 0; lastIndex--) { + if (predicate(array[lastIndex])) { + return array[lastIndex]; + } + } +} + +/** + * Groups array items based on elementSelector function + * @param array The array to process. + * @param elementSelector Function invoked per iteration. + */ +export function groupBy(array: T[], elementSelector: Selector): Array { + const groups: { [key: string]: T[] } = {}; + const length = array.length; + + for (let i = 0; i < length; i++) { + const item = array[i]; + const group = elementSelector(item); + groups[group] = groups[group] || []; + groups[group].push(item); + } + + return Object.values(groups); +} + +/** + * Flattens array. + * @param array The array to flatten recursively. + * @example + * flatten([1, 4, [2, [5, 5, [9, 7]], 11], 0]); // length 9 + */ +export function flatten(array: any[]): any[] { + let res: any = []; + const length = array.length; + + for (let i = 0; i < length; i++) { + var value = array[i]; + if (Array.isArray(value)) { + res = [...res, ...flatten(value)]; + } else { + res.push(value); + } + } + return res; +} + +/** + * Gets counts map of values returned by `elementSelector`. + * @param array The array to process. + * @param elementSelector Function invoked per iteration. + */ +export function countBy(array: any[], elementSelector: Selector): { [key: string]: number } { + const results: { [key: string]: number } = {}; + const length = array.length; + + for (let i = 0; i < length; i++) { + const item = array[i]; + const group = elementSelector(item); + results[group] = results[group] || 0; + results[group]++; + } + + return results; +} + +/** + * Joins data in array by elementSelector functions. + * @param array The array to process. + * @param array2 The array to join. + * @param elementSelector Gets key value of array. + * @param elementSelector2 Gets key value of array2. + */ +export function joinArray(array: any[], array2: object[], elementSelector: Selector, elementSelector2: Selector): any[] { + const length = array.length; + const res = []; + for (let i = 0; i < length; i++) { + const item = array[i]; + const item2 = array2.find(item2 => elementSelector(item) === elementSelector2(item2)); + res.push({ ...item, ...item2 }) + } + + return res; +} + +/** + * Get selector number values. + * @param array The array to process. + * @param elementSelector Function invoked per iteration. + * @private + */ +function getNumberValuesArray(array: any[], elementSelector?: Selector): number[] { + return array.map(item => getNumberValue(item, elementSelector)).filter(v => v !== undefined) as number[]; +} + +/** + * Formats selected value to number. + * @private + * @param val Primitive or object. + * @param elementSelector Function invoked per iteration. + */ +function getNumberValue(val: any, elementSelector?: Selector): number | undefined { + if (elementSelector && typeof elementSelector === 'function') { + val = elementSelector(val); + } + switch (typeof val) { + case 'string': return parseFloat(val); + case 'boolean': return Number(val); + case 'number': return val; + } +} diff --git a/src/data-pipe.spec.ts b/src/data-pipe.spec.ts new file mode 100644 index 0000000..409a431 --- /dev/null +++ b/src/data-pipe.spec.ts @@ -0,0 +1,31 @@ +import dataPipe from './index'; +import { DataPipe } from './data-pipe'; + +describe('DataPipe specification', () => { + + const data = [ + { name: "John", country: "US" }, { name: "Joe", country: "US" }, { name: "Bill", country: "US" }, { name: "Adam", country: "UK" }, + { name: "Scott", country: "UK" }, { name: "Diana", country: "UK" }, { name: "Marry", country: "FR" }, { name: "Luc", country: "FR" } + ] + + it('dataPipe returns DataPipe', () => { + expect(dataPipe([]) instanceof DataPipe).toBeTruthy(); + }); + + it('toArray', () => { + const arr = dataPipe(['US']).toArray(); + expect(arr instanceof Array).toBeTruthy(); + expect(arr).toEqual(['US']); + }) + + it('select/map', () => { + const arr = [{ country: 'US' }]; + expect(dataPipe(arr).select(i => i.country).toArray()).toEqual(['US']); + expect(dataPipe(arr).map(i => i.country).toArray()).toEqual(['US']); + }) + + it('groupBy', () => { + const dp = dataPipe(data).groupBy(i => i.country) + expect(dp.toArray().length).toBe(3); + }) +}) diff --git a/src/data-pipe.ts b/src/data-pipe.ts new file mode 100644 index 0000000..35a57f7 --- /dev/null +++ b/src/data-pipe.ts @@ -0,0 +1,152 @@ +import { sum, avg, count, min, max, first, last, groupBy, flatten, countBy, joinArray } from './array'; +import { Selector, Predicate } from './models'; + +export class DataPipe { + private data: Array; + + constructor(data: T[]) { + this.data = data; + } + + /** + * Get pipes currrent array data. + */ + toArray(): T[] { + return this.data; + } + + /** + * Sum of items in array. + * @param elementSelector Function invoked per iteration. + * @example + * dataPipe([1, 2, 5]).sum(); // 8 + * + * dataPipe([{ val: 1 }, { val: 5 }]).sum(i => i.val); // 6 + */ + sum(elementSelector?: Selector): number | undefined { + return sum(this.data, elementSelector); + } + + /** + * Select data from array item. + * @param elementSelector Function invoked per iteration. + * @example + * + * dataPipe([{ val: 1 }, { val: 5 }]).select(i => i.val).toArray(); // [1, 5] + */ + select(elementSelector: Selector): DataPipe { + this.data = this.data.map(elementSelector); + return this; + } + + map = this.select.bind(this); + + /** + * Average of array items. + * @param elementSelector Function invoked per iteration. + * @example + * dataPipe([1, 5, 3]).avg(); // 3 + */ + avg(elementSelector?: Selector): number | undefined { + return avg(this.data, elementSelector); + } + + average = this.avg.bind(this); + + /** + * Count of elements in array. + * @param predicate Predicate function invoked per iteration. + */ + count(predicate?: Predicate): number | undefined { + return count(this.data, predicate); + } + + /** + * Computes the minimum value of array. + * @param elementSelector Function invoked per iteration. + */ + min(elementSelector?: Selector): number | undefined { + return min(this.data, elementSelector); + } + + /** + * Computes the maximum value of array. + * @param elementSelector Function invoked per iteration. + */ + max(elementSelector?: Selector): number | null { + return max(this.data, elementSelector); + } + + /** + * Gets first item in array satisfies predicate. + * @param predicate Predicate function invoked per iteration. + */ + first(predicate?: Predicate): T | undefined { + return first(this.data, predicate); + } + + /** + * Gets last item in array satisfies predicate. + * @param array The array to process. + * @param predicate Predicate function invoked per iteration. + */ + last(predicate?: Predicate): T | undefined { + return last(this.data, predicate); + } + + /** + * Groups array items based on elementSelector function + * @param elementSelector Function invoked per iteration. + */ + groupBy(elementSelector: Selector): DataPipe { + this.data = groupBy(this.data, elementSelector); + return this; + } + + /** + * Flattens array. + * @example + * dataPipe([1, 4, [2, [5, 5, [9, 7]], 11], 0]).flatten(); // length 9 + */ + flatten(): T[] { + return flatten(this.data); + } + + /** + * Gets counts map of values returned by `elementSelector`. + * @param elementSelector Function invoked per iteration. + */ + countBy(elementSelector: Selector): { [key: string]: number } { + return countBy(this.data, elementSelector); + } + + /** + * Joins data in array by iteratee functions. + * @param array2 The array to join. + * @param keySelector1 Gets key value of array. + * @param keySelector1 Gets key value of array2. + */ + joinArray(array2: object[], keySelector1: Selector, keySelector2: Selector): DataPipe { + this.data = joinArray(this.data, array2, keySelector1, keySelector2); + return this; + } + + /** + * Gets joined as sring array items. + * @param separator String separator. + */ + join(separator?: string): string { + return this.data.join(separator); + } + + /** + * Filters array of items. + * @param predicate Predicate function invoked per iteration. + */ + where(predicate: Predicate): DataPipe { + this.data = this.data.filter(predicate); + return this; + } + + filter = this.where.bind(this); +} diff --git a/src/examples/node-example.js b/src/examples/node-example.js new file mode 100644 index 0000000..ca2c59c --- /dev/null +++ b/src/examples/node-example.js @@ -0,0 +1,20 @@ +const dataPipe = require("../../lib/data-pipe.umd").dataPipe; + +const data = [ + { name: "John", country: "US"}, { name: "Joe", country: "US"}, { name: "Bill", country: "US"}, { name: "Adam", country: "UK"}, + { name: "Scott", country: "UK"}, { name: "Diana",country: "UK"}, { name: "Marry",country: "FR"}, { name: "Luc",country: "FR"} +] + +const summaryForUS = dataPipe(data) + .groupBy(i => i.country) + .select(g => { + const r = {} + r.country = dataPipe(g).first().country + r.names = dataPipe(g).map(r => r.name).join(", ") + r.count = dataPipe(g).count() + return r; + }) + .where(r => r.country === "US") + .toArray() + + console.log(summaryForUS); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c8d17aa --- /dev/null +++ b/src/index.ts @@ -0,0 +1,12 @@ +import { DataPipe } from "./data-pipe"; + +/** + * Data Pipeline factory function what creates DataPipe + * @param data Initial array + * + * @example + * dataPipe([1, 2, 3]) + */ +export default function dataPipe(data: T[]): DataPipe { + return new DataPipe(data); +} diff --git a/src/models.ts b/src/models.ts new file mode 100644 index 0000000..69b12d1 --- /dev/null +++ b/src/models.ts @@ -0,0 +1,2 @@ +export type Predicate = (p: T) => boolean; +export type Selector = (p: T) => V; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ffe5302 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,80 @@ +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ], + "typedocOptions": { + "mode": "modules", + "out": "docs", + "excludeExternals": true, + "exclude": "**/*+(index|.spec|.e2e).ts", + "readme": "README.md" + } +}