[READ-ONLY] Mirror of https://github.com/FoxxMD/string-sameness. Compare the sameness of two strings foxxmd.github.io/string-sameness/
compare cosine-similarity dice-coefficient levenshtein-distance sameness string text typescript
0

Configure Feed

Select the types of activity you want to include in your feed.

string-sameness / src / index.ts
6.0 kB 182 lines
1import {cosineStrategy, levenStrategy, diceStrategy, cosineStrategyAggressive} from "./matchingStrategies/index.js"; 2import { 3 ComparisonStrategyResult, 4 NamedComparisonStrategyObjectResult, 5 StringComparisonOptions, 6 StringSamenessResult, 7 StringTransformFunc 8} from "./atomic.js"; 9import {strDefaultTransforms, transforms} from "./normalization/index.js"; 10 11const sentenceLengthWeight = (length: number) => { 12 // thanks jordan :') 13 // constants are black magic 14 return (Math.log(length) / 0.20) - 5; 15} 16 17const defaultStrategies = [ 18 diceStrategy, 19 levenStrategy, 20 cosineStrategy 21] 22 23const stringSameness = (valA: string, valB: string, options?: StringComparisonOptions): StringSamenessResult => { 24 25 const { 26 transforms = strDefaultTransforms, 27 strategies = defaultStrategies, 28 reorder = false, 29 delimiter = ' ' 30 } = options || {}; 31 32 let cleanA = transforms.reduce((acc, curr) => curr(acc), valA); 33 let cleanB = transforms.reduce((acc, curr) => curr(acc), valB); 34 35 if (reorder) { 36 // we want to ignore order of tokens as much as possible (user does not care about differences in word order, just absolute differences in characters overall) 37 // so we will reorder the shorter of the two strings so its tokens match the order of tokens in the longer string as closely as possible 38 // before we run strategies 39 const [orderedX, orderedY] = reorderStr(cleanA, cleanB); 40 cleanA = orderedX; 41 cleanB = orderedY; 42 } 43 44 const shortest = cleanA.length > cleanB.length ? cleanB : cleanA; 45 46 const stratResults: NamedComparisonStrategyObjectResult[] = []; 47 48 for (const strat of strategies) { 49 if (strat.isValid !== undefined && !strat.isValid(cleanA, cleanB)) { 50 continue; 51 } 52 const res = strat.strategy(cleanA, cleanB); 53 const resObj = typeof res === 'number' ? {score: res} : res; 54 stratResults.push({ 55 ...resObj, 56 name: strat.name 57 }); 58 } 59 60 // use shortest sentence for weight 61 const weightScore = sentenceLengthWeight(shortest.length); 62 63 // take average score 64 const highScore = stratResults.reduce((acc, curr) => acc + curr.score, 0) / stratResults.length; 65 // weight score can be a max of 15 66 const highScoreWeighted = highScore + Math.min(weightScore, 15); 67 const stratObj = stratResults.reduce((acc: { [key: string]: ComparisonStrategyResult }, curr) => { 68 const {name, score, ...rest} = curr; 69 acc[curr.name] = { 70 ...rest, 71 score, 72 }; 73 return acc; 74 }, {}); 75 return { 76 strategies: stratObj, 77 highScore, 78 highScoreWeighted, 79 } 80} 81 82export const reorderStr = (strA: string, strB: string, options?: StringComparisonOptions): [string, string] => { 83 84 const { 85 transforms = strDefaultTransforms, 86 strategies = defaultStrategies, 87 delimiter = ' ' 88 } = options || {}; 89 90 const cleanA = transforms.reduce((acc, curr) => curr(acc), strA); 91 const cleanB = transforms.reduce((acc, curr) => curr(acc), strB); 92 93 // split by "token" 94 const eTokens = cleanA.split(delimiter); 95 const cTokens = cleanB.split(delimiter); 96 97 98 let longerTokens: string[], 99 shorterTokens: string[]; 100 101 if (eTokens.length > cTokens.length) { 102 longerTokens = eTokens; 103 shorterTokens = cTokens; 104 } else { 105 longerTokens = cTokens; 106 shorterTokens = eTokens; 107 } 108 109 // we will use longest string (token list) as the reducer and order the shorter list to match it 110 // so we don't have to deal with undefined positions in the shorter list 111 112 const orderedCandidateTokens = longerTokens.reduce((acc: { ordered: string[], remaining: string[] }, curr) => { 113 // if we've run out of tokens in the shorter list just return 114 if (acc.remaining.length === 0) { 115 return acc; 116 } 117 118 // on each iteration of tokens in the long list 119 // we iterate through remaining tokens from the shorter list and find the token with the most sameness 120 121 let highScore = 0; 122 let highIndex = 0; 123 let index = 0; 124 for (const token of acc.remaining) { 125 const result = stringSameness(curr, token, {strategies}); 126 if (result.highScoreWeighted > highScore) { 127 highScore = result.highScoreWeighted; 128 highIndex = index; 129 } 130 index++; 131 } 132 133 // then remove the most same token from the remaining short list tokens 134 const splicedRemaining = [...acc.remaining]; 135 splicedRemaining.splice(highIndex, 1); 136 137 return { 138 // finally add the most same token to the ordered short list 139 ordered: acc.ordered.concat(acc.remaining[highIndex]), 140 // and return the remaining short list tokens 141 remaining: splicedRemaining 142 }; 143 }, { 144 // "ordered" is the result of ordering tokens in the shorter list to match longer token order 145 ordered: [], 146 // remaining is the initial shorter list 147 remaining: shorterTokens 148 }); 149 150 return [longerTokens.join(' '), orderedCandidateTokens.ordered.join(' ')]; 151} 152 153const createStringSameness = (defaults: StringComparisonOptions) => { 154 return (valA: string, valB: string, options: StringComparisonOptions = {}) => stringSameness(valA, valB, {...defaults, ...options}); 155} 156 157const strategies = { 158 diceStrategy, 159 levenStrategy, 160 cosineStrategy, 161 cosineStrategyAggressive 162}; 163 164// maintaining compatibility 165const defaultStrCompareTransformFuncs = strDefaultTransforms; 166 167export { 168 stringSameness, 169 createStringSameness, 170 defaultStrategies, 171 strategies, 172 transforms, 173 defaultStrCompareTransformFuncs, 174 strDefaultTransforms, 175} 176 177export type { 178 StringSamenessResult, 179 StringComparisonOptions, 180 ComparisonStrategyResult, 181 StringTransformFunc 182}