mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 10:54:44 +00:00
7
cli.ts
7
cli.ts
@@ -7,6 +7,7 @@ const PACKAGE_MANAGER = 'pnpm@8.15.6';
|
||||
const NODE_VERSION = '>=18.0.0';
|
||||
const VITE_VERSION = '^5.2.8';
|
||||
const VITE_DTS_VERSION = '^3.8.1';
|
||||
const PATHE_VERSION = '^1.1.2'
|
||||
const DEFAULT_DIR = 'packages';
|
||||
|
||||
const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
|
||||
@@ -29,13 +30,13 @@ const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
|
||||
},
|
||||
type: 'module',
|
||||
files: ['dist'],
|
||||
main: './dist/index.cjs',
|
||||
main: './dist/index.umd.js',
|
||||
module: './dist/index.js',
|
||||
types: './dist/index.d.ts',
|
||||
exports: {
|
||||
'.': {
|
||||
import: './dist/index.js',
|
||||
require: './dist/index.cjs',
|
||||
require: './dist/index.umd.js',
|
||||
types: './dist/index.d.ts',
|
||||
},
|
||||
},
|
||||
@@ -52,6 +53,7 @@ const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
|
||||
...(hasVite && {
|
||||
vite: VITE_VERSION,
|
||||
'vite-plugin-dts': VITE_DTS_VERSION,
|
||||
pathe: PATHE_VERSION,
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -61,6 +63,7 @@ const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
|
||||
|
||||
const generateViteConfig = () => `import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import { resolve } from 'pathe';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
|
||||
1
packages/stdlib/README.md
Normal file
1
packages/stdlib/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# @robonen/stdlib
|
||||
55
packages/stdlib/package.json
Normal file
55
packages/stdlib/package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@robonen/stdlib",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "UNLICENSED",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"stdlib",
|
||||
"utils",
|
||||
"tools",
|
||||
"helpers",
|
||||
"math",
|
||||
"algorithms",
|
||||
"data-structures"
|
||||
],
|
||||
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/robonen/tools.git",
|
||||
"directory": "packages/stdlib"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.6",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/stdlib.umd.js",
|
||||
"module": "./dist/stdlib.js",
|
||||
"types": "./dist/stdlib.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/stdlib.js",
|
||||
"require": "./dist/stdlib.umd.js",
|
||||
"types": "./dist/stdlib.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:bench": "vitest bench",
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@robonen/tsconfig": "workspace:*",
|
||||
"@vitest/coverage-v8": "^1.4.0",
|
||||
"pathe": "^1.1.2",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-dts": "^3.8.1",
|
||||
"vitest": "^1.4.0"
|
||||
}
|
||||
}
|
||||
2
packages/stdlib/src/index.ts
Normal file
2
packages/stdlib/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './text';
|
||||
export * from './math';
|
||||
46
packages/stdlib/src/math/clamp/index.test.ts
Normal file
46
packages/stdlib/src/math/clamp/index.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe,it, expect } from 'vitest';
|
||||
import { clamp } from '.';
|
||||
|
||||
describe('clamp', () => {
|
||||
it('clamp a value within the given range', () => {
|
||||
// value < min
|
||||
expect(clamp(-10, 0, 100)).toBe(0);
|
||||
|
||||
// value > max
|
||||
expect(clamp(200, 0, 100)).toBe(100);
|
||||
|
||||
// value within range
|
||||
expect(clamp(50, 0, 100)).toBe(50);
|
||||
|
||||
// value at min
|
||||
expect(clamp(0, 0, 100)).toBe(0);
|
||||
|
||||
// value at max
|
||||
expect(clamp(100, 0, 100)).toBe(100);
|
||||
|
||||
// value at midpoint
|
||||
expect(clamp(50, 100, 100)).toBe(100);
|
||||
});
|
||||
|
||||
it('handle floating-point numbers correctly', () => {
|
||||
// floating-point value within range
|
||||
expect(clamp(3.14, 0, 5)).toBe(3.14);
|
||||
|
||||
// floating-point value < min
|
||||
expect(clamp(-1.5, 0, 10)).toBe(0);
|
||||
|
||||
// floating-point value > max
|
||||
expect(clamp(15.75, 0, 10)).toBe(10);
|
||||
});
|
||||
|
||||
it('handle edge cases', () => {
|
||||
// all values are the same
|
||||
expect(clamp(5, 5, 5)).toBe(5);
|
||||
|
||||
// min > max
|
||||
expect(clamp(10, 100, 50)).toBe(50);
|
||||
|
||||
// negative range and value
|
||||
expect(clamp(-10, -100, -5)).toBe(-10);
|
||||
});
|
||||
});
|
||||
16
packages/stdlib/src/math/clamp/index.ts
Normal file
16
packages/stdlib/src/math/clamp/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Clamps a number between a minimum and maximum value
|
||||
*
|
||||
* @param {number} value The number to clamp
|
||||
* @param {number} min Minimum value
|
||||
* @param {number} max Maximum value
|
||||
* @returns {number} The clamped number
|
||||
*/
|
||||
export function clamp(value: number, min: number, max: number): number {
|
||||
// The clamp function takes a value, a minimum, and a maximum as parameters.
|
||||
// It ensures that the value falls within the range defined by the minimum and maximum values.
|
||||
// If the value is less than the minimum, it returns the minimum value.
|
||||
// If the value is greater than the maximum, it returns the maximum value.
|
||||
// Otherwise, it returns the original value.
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
2
packages/stdlib/src/math/index.ts
Normal file
2
packages/stdlib/src/math/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './clamp';
|
||||
export * from './mapRange';
|
||||
46
packages/stdlib/src/math/mapRange/index.test.ts
Normal file
46
packages/stdlib/src/math/mapRange/index.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { mapRange } from './index';
|
||||
|
||||
describe('mapRange', () => {
|
||||
it('map values from one range to another', () => {
|
||||
// value at midpoint
|
||||
expect(mapRange(5, 0, 10, 0, 100)).toBe(50);
|
||||
|
||||
// value at min
|
||||
expect(mapRange(0, 0, 10, 0, 100)).toBe(0);
|
||||
|
||||
// value at max
|
||||
expect(mapRange(10, 0, 10, 0, 100)).toBe(100);
|
||||
|
||||
// value outside range (below)
|
||||
expect(mapRange(-5, 0, 10, 0, 100)).toBe(0);
|
||||
|
||||
// value outside range (above)
|
||||
expect(mapRange(15, 0, 10, 0, 100)).toBe(100);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(mapRange(75, 50, 100, -50, 50)).toBe(0);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(mapRange(-25, -50, 0, 0, 100)).toBe(50);
|
||||
});
|
||||
|
||||
it('handle floating-point numbers correctly', () => {
|
||||
// floating-point value
|
||||
expect(mapRange(3.5, 0, 10, 0, 100)).toBe(35);
|
||||
|
||||
// positive floating-point ranges
|
||||
expect(mapRange(1.25, 0, 2.5, 0, 100)).toBe(50);
|
||||
|
||||
// negative floating-point value
|
||||
expect(mapRange(-2.5, -5, 0, 0, 100)).toBe(50);
|
||||
|
||||
// negative floating-point ranges
|
||||
expect(mapRange(-1.25, -2.5, 0, 0, 100)).toBe(50);
|
||||
});
|
||||
|
||||
it('handle edge cases', () => {
|
||||
// input range is zero (should return output min)
|
||||
expect(mapRange(5, 0, 0, 0, 100)).toBe(0);
|
||||
});
|
||||
});
|
||||
23
packages/stdlib/src/math/mapRange/index.ts
Normal file
23
packages/stdlib/src/math/mapRange/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { clamp } from "../clamp";
|
||||
|
||||
/**
|
||||
* Map a value from one range to another
|
||||
*
|
||||
* @param {number} value The value to map
|
||||
* @param {number} in_min The minimum value of the input range
|
||||
* @param {number} in_max The maximum value of the input range
|
||||
* @param {number} out_min The minimum value of the output range
|
||||
* @param {number} out_max The maximum value of the output range
|
||||
* @returns {number} The mapped value
|
||||
*/
|
||||
export function mapRange(value: number, in_min: number, in_max: number, out_min: number, out_max: number): number {
|
||||
// Zero input range means invalid input, so return lowest output range value
|
||||
if (in_min === in_max)
|
||||
return out_min;
|
||||
|
||||
// To ensure the value is within the input range, clamp it
|
||||
const clampedValue = clamp(value, in_min, in_max);
|
||||
|
||||
// Finally, map the value from the input range to the output range
|
||||
return (clampedValue - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
||||
}
|
||||
2
packages/stdlib/src/text/index.ts
Normal file
2
packages/stdlib/src/text/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './levenshtein-distance';
|
||||
export * from './trigram-distance';
|
||||
32
packages/stdlib/src/text/levenshtein-distance/index.test.ts
Normal file
32
packages/stdlib/src/text/levenshtein-distance/index.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {levenshteinDistance} from '.';
|
||||
|
||||
describe('levenshteinDistance', () => {
|
||||
it('calculate edit distance between two strings', () => {
|
||||
// just one substitution I at the beginning
|
||||
expect(levenshteinDistance('islander', 'slander')).toBe(1);
|
||||
|
||||
// substitution M->K, T->M and add an A to the end
|
||||
expect(levenshteinDistance('mart', 'karma')).toBe(3);
|
||||
|
||||
// substitution K->S, E->I and insert G at the end
|
||||
expect(levenshteinDistance('kitten', 'sitting')).toBe(3);
|
||||
|
||||
// should add 4 letters FOOT at the beginning
|
||||
expect(levenshteinDistance('ball', 'football')).toBe(4);
|
||||
|
||||
// should delete 4 letters FOOT at the beginning
|
||||
expect(levenshteinDistance('football', 'foot')).toBe(4);
|
||||
|
||||
// needs to substitute the first 5 chars INTEN->EXECU
|
||||
expect(levenshteinDistance('intention', 'execution')).toBe(5);
|
||||
});
|
||||
|
||||
it('handle empty strings', () => {
|
||||
expect(levenshteinDistance('', '')).toBe(0);
|
||||
expect(levenshteinDistance('a', '')).toBe(1);
|
||||
expect(levenshteinDistance('', 'a')).toBe(1);
|
||||
expect(levenshteinDistance('abc', '')).toBe(3);
|
||||
expect(levenshteinDistance('', 'abc')).toBe(3);
|
||||
});
|
||||
});
|
||||
44
packages/stdlib/src/text/levenshtein-distance/index.ts
Normal file
44
packages/stdlib/src/text/levenshtein-distance/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Calculate the Levenshtein distance between two strings
|
||||
*
|
||||
* @param {string} left First string
|
||||
* @param {string} right Second string
|
||||
* @returns {number} The Levenshtein distance between the two strings
|
||||
*/
|
||||
export function levenshteinDistance(left: string, right: string): number {
|
||||
// If the strings are equal, the distance is 0
|
||||
if (left === right) return 0;
|
||||
|
||||
// If either string is empty, the distance is the length of the other string
|
||||
if (left.length === 0) return right.length;
|
||||
if (right.length === 0) return left.length;
|
||||
|
||||
// Create empty edit distance matrix for all possible modifications of
|
||||
// substrings of left to substrings of right
|
||||
const distanceMatrix = Array(right.length + 1).fill(null).map(() => Array(left.length + 1).fill(null));
|
||||
|
||||
// Fill the first row of the matrix
|
||||
// If this is the first row, we're transforming from an empty string to left
|
||||
// In this case, the number of operations equals the length of left substring
|
||||
for (let i = 0; i <= left.length; i++)
|
||||
distanceMatrix[0]![i]! = i;
|
||||
|
||||
// Fill the first column of the matrix
|
||||
// If this is the first column, we're transforming empty string to right
|
||||
// In this case, the number of operations equals the length of right substring
|
||||
for (let j = 0; j <= right.length; j++)
|
||||
distanceMatrix[j]![0]! = j;
|
||||
|
||||
for (let j = 1; j <= right.length; j++) {
|
||||
for (let i = 1; i <= left.length; i++) {
|
||||
const indicator = left[i - 1] === right[j - 1] ? 0 : 1;
|
||||
distanceMatrix[j]![i]! = Math.min(
|
||||
distanceMatrix[j]![i - 1]! + 1, // deletion
|
||||
distanceMatrix[j - 1]![i]! + 1, // insertion
|
||||
distanceMatrix[j - 1]![i - 1]! + indicator // substitution
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return distanceMatrix[right.length]![left.length]!;
|
||||
}
|
||||
93
packages/stdlib/src/text/trigram-distance/index.test.ts
Normal file
93
packages/stdlib/src/text/trigram-distance/index.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { trigramDistance, trigramProfile } from '.';
|
||||
|
||||
describe('trigramProfile', () => {
|
||||
it('trigram profile of a text with different trigrams', () => {
|
||||
const different_trigrams = 'hello world';
|
||||
const profile1 = trigramProfile(different_trigrams);
|
||||
|
||||
expect(profile1).toEqual(new Map([
|
||||
['\n\nh', 1],
|
||||
['\nhe', 1],
|
||||
['hel', 1],
|
||||
['ell', 1],
|
||||
['llo', 1],
|
||||
['lo ', 1],
|
||||
['o w', 1],
|
||||
[' wo', 1],
|
||||
['wor', 1],
|
||||
['orl', 1],
|
||||
['rld', 1],
|
||||
['ld\n', 1],
|
||||
['d\n\n', 1]
|
||||
]));
|
||||
});
|
||||
|
||||
it('trigram profile of a text with repeated trigrams', () => {
|
||||
const repeated_trigrams = 'hello hello';
|
||||
const profile2 = trigramProfile(repeated_trigrams);
|
||||
|
||||
expect(profile2).toEqual(new Map([
|
||||
['\n\nh', 1],
|
||||
['\nhe', 1],
|
||||
['hel', 2],
|
||||
['ell', 2],
|
||||
['llo', 2],
|
||||
['lo ', 1],
|
||||
['o h', 1],
|
||||
[' he', 1],
|
||||
['lo\n', 1],
|
||||
['o\n\n', 1]
|
||||
]));
|
||||
});
|
||||
|
||||
it('trigram profile of an empty text', () => {
|
||||
const text = '';
|
||||
const profile = trigramProfile(text);
|
||||
|
||||
expect(profile).toEqual(new Map([
|
||||
['\n\n\n', 2],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('trigramDistance', () => {
|
||||
it('zero when comparing the same text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('hello world');
|
||||
|
||||
expect(trigramDistance(profile1, profile2)).toBe(0);
|
||||
});
|
||||
|
||||
it('one for completely different text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('lorem ipsum');
|
||||
|
||||
expect(trigramDistance(profile1, profile2)).toBe(1);
|
||||
});
|
||||
|
||||
it('one for empty text and non-empty text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('');
|
||||
|
||||
expect(trigramDistance(profile1, profile2)).toBe(1);
|
||||
});
|
||||
|
||||
it('approximately 0.5 for similar text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('hello lorem');
|
||||
|
||||
const approx = trigramDistance(profile1, profile2);
|
||||
|
||||
expect(approx).toBeGreaterThan(0.45);
|
||||
expect(approx).toBeLessThan(0.55);
|
||||
});
|
||||
|
||||
it('triangle inequality', () => {
|
||||
const A = trigramDistance(trigramProfile('metric'), trigramProfile('123ric'));
|
||||
const B = trigramDistance(trigramProfile('123ric'), trigramProfile('123456'));
|
||||
const C = trigramDistance(trigramProfile('metric'), trigramProfile('123456'));
|
||||
|
||||
expect(A + B).toBeGreaterThanOrEqual(C);
|
||||
});
|
||||
});
|
||||
49
packages/stdlib/src/text/trigram-distance/index.ts
Normal file
49
packages/stdlib/src/text/trigram-distance/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export type Trigrams = Map<string, number>;
|
||||
|
||||
/**
|
||||
* Extracts trigrams from a text and returns a map of trigram to count
|
||||
*
|
||||
* @param {string} text The text to extract trigrams
|
||||
* @returns {Trigrams} A map of trigram to count
|
||||
*/
|
||||
export function trigramProfile(text: string): Trigrams {
|
||||
text = '\n\n' + text + '\n\n';
|
||||
|
||||
const trigrams = new Map<string, number>();
|
||||
|
||||
for (let i = 0; i < text.length - 2; i++) {
|
||||
const trigram = text.slice(i, i + 3);
|
||||
const count = trigrams.get(trigram) ?? 0;
|
||||
trigrams.set(trigram, count + 1);
|
||||
}
|
||||
|
||||
return trigrams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the trigram distance between two strings
|
||||
*
|
||||
* @param {Trigrams} left First text trigram profile
|
||||
* @param {Trigrams} right Second text trigram profile
|
||||
* @returns {number} The trigram distance between the two strings
|
||||
*/
|
||||
export function trigramDistance(left: Trigrams, right: Trigrams) {
|
||||
let distance = -4;
|
||||
let total = -4;
|
||||
|
||||
for (const [trigram, left_count] of left) {
|
||||
total += left_count;
|
||||
const right_count = right.get(trigram) ?? 0;
|
||||
distance += Math.abs(left_count - right_count);
|
||||
}
|
||||
|
||||
for (const [trigram, right_count] of right) {
|
||||
total += right_count;
|
||||
const left_count = left.get(trigram) ?? 0;
|
||||
distance += Math.abs(left_count - right_count);
|
||||
}
|
||||
|
||||
if (distance < 0) return 0;
|
||||
|
||||
return distance / total;
|
||||
}
|
||||
3
packages/stdlib/tsconfig.json
Normal file
3
packages/stdlib/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@robonen/tsconfig/tsconfig.json",
|
||||
}
|
||||
24
packages/stdlib/vite.config.ts
Normal file
24
packages/stdlib/vite.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import { resolve } from 'pathe';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
name: 'Stdlib',
|
||||
fileName: 'stdlib',
|
||||
entry: resolve(__dirname, './src/index.ts'),
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
exclude: '**/*.test.ts',
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "UNLICENSED",
|
||||
"description": "",
|
||||
"description": "Base typescript configuration for projects",
|
||||
"keywords": [
|
||||
"tsconfig",
|
||||
"typescript",
|
||||
@@ -22,5 +22,8 @@
|
||||
},
|
||||
"files": [
|
||||
"**tsconfig.json"
|
||||
]
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,8 @@
|
||||
"module": "Preserve",
|
||||
"noEmit": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"sourceMap": true,
|
||||
"target": "ESNext",
|
||||
|
||||
"outDir": "dist",
|
||||
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -27,10 +26,9 @@
|
||||
|
||||
/* Library transpiling */
|
||||
"declaration": true,
|
||||
|
||||
/* Library in monorepo */
|
||||
"composite": true,
|
||||
"declarationMap": true
|
||||
"sourceMap": false,
|
||||
"declarationMap": false
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
1134
pnpm-lock.yaml
generated
1134
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
10
tools.code-workspace
Normal file
10
tools.code-workspace
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "packages/stdlib"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user