1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 10:54:44 +00:00

Merge pull request #2 from robonen/stdlib

Stdlib
This commit is contained in:
2024-04-12 02:11:49 +07:00
committed by GitHub
21 changed files with 1615 additions and 29 deletions

7
cli.ts
View File

@@ -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: {

View File

@@ -0,0 +1 @@
# @robonen/stdlib

View 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"
}
}

View File

@@ -0,0 +1,2 @@
export * from './text';
export * from './math';

View 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);
});
});

View 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);
}

View File

@@ -0,0 +1,2 @@
export * from './clamp';
export * from './mapRange';

View 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);
});
});

View 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;
}

View File

@@ -0,0 +1,2 @@
export * from './levenshtein-distance';
export * from './trigram-distance';

View 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);
});
});

View 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]!;
}

View 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);
});
});

View 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;
}

View File

@@ -0,0 +1,3 @@
{
"extends": "@robonen/tsconfig/tsconfig.json",
}

View 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',
}),
],
});

View File

@@ -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"
}
}

View File

@@ -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"]
}

1172
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

10
tools.code-workspace Normal file
View File

@@ -0,0 +1,10 @@
{
"folders": [
{
"path": "."
},
{
"path": "packages/stdlib"
}
]
}