Skip to content

Commit ef7c8fa

Browse files
Merge branch 'dev'
2 parents 3204aa9 + 12e6c80 commit ef7c8fa

File tree

2 files changed

+207
-113
lines changed

2 files changed

+207
-113
lines changed

src/index.ts

Lines changed: 112 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import type { NestedKeyOf } from './types'
22
import { Effect } from 'effect'
3-
import { normalizeText } from './utils'
3+
import { addBitVectors, bitwiseAND, bitwiseNOT, bitwiseOR, bitwiseShiftLeft, bitwiseXOR, countSetBits, maskVP, normalizeText, setLeastSignificantBit } from './utils'
44

55
export class QuantumMatcher<T> {
66
private collection: T[]
7-
87
private pathCache = new Map<string, string[]>()
98

109
constructor(
@@ -19,7 +18,7 @@ export class QuantumMatcher<T> {
1918
public findMatches(query: string): { item: T, score: number, matches: [number, number][] }[] {
2019
return Effect.try({
2120
try: () => {
22-
const queryParts = query.split(' ').map(normalizeText) // Split and normalize query parts
21+
const queryParts = query.split(' ').map(normalizeText)
2322
const results: { item: T, score: number, matches: [number, number][] }[] = []
2423

2524
for (const item of this.collection) {
@@ -37,26 +36,30 @@ export class QuantumMatcher<T> {
3736
const m = queryPart.length
3837
const n = normalizedText.length
3938

40-
let VP = (1n << BigInt(m)) - 1n
41-
let HN = 0n
42-
let HP = 0n
39+
let VP = this.createInitialVP(m)
40+
let HN: number[] = []
41+
let HP: number[] = []
4342
let score = 0
4443
let matches: [number, number][] = []
4544

4645
for (let j = 0; j < n; j++) {
4746
const char = normalizedText[j]
48-
const EQ = queryMask.get(char) || 0n
47+
const EQ = queryMask.get(char) || []
48+
49+
const X = bitwiseOR(EQ, HP)
50+
const X_and_VP = bitwiseAND(X, VP)
51+
const sum = addBitVectors(VP, X_and_VP)
52+
const sum_xor_VP = bitwiseXOR(sum, VP)
53+
const D0 = bitwiseOR(sum_xor_VP, X)
4954

50-
const X = EQ | HP
51-
const D0 = ((VP + (X & VP)) ^ VP) | X
52-
HN = VP & D0
53-
HP = VP | ~(D0 | HN)
55+
const HN_new = bitwiseAND(VP, D0)
56+
const D0_or_HN = bitwiseOR(D0, HN_new)
57+
const HP_new = bitwiseOR(VP, bitwiseNOT(D0_or_HN))
5458

55-
HP = (HP << 1n) | 1n
56-
HN <<= 1n
59+
HP = setLeastSignificantBit(bitwiseShiftLeft(HP_new))
60+
HN = bitwiseShiftLeft(HN_new)
5761

58-
VP = HP | ~(D0 | HN)
59-
VP &= (1n << BigInt(m)) - 1n
62+
VP = maskVP(bitwiseOR(HP, bitwiseNOT(bitwiseOR(D0, HN))), m)
6063

6164
const currentScore = this.calculateMatchQuality(VP, m, j, normalizedText, queryPart)
6265
if (currentScore > score) {
@@ -75,7 +78,6 @@ export class QuantumMatcher<T> {
7578
allMatches = allMatches.concat(bestMatchesForPart)
7679
}
7780

78-
// Only add results with a significant total score
7981
if (totalScore / queryParts.length > 0.5) {
8082
results.push({ item, score: totalScore / queryParts.length, matches: allMatches })
8183
}
@@ -93,129 +95,126 @@ export class QuantumMatcher<T> {
9395
if (obj == null)
9496
return undefined
9597

96-
// Cache path arrays
9798
let pathArray = this.pathCache.get(path)
9899
if (!pathArray) {
99100
pathArray = path.split('.')
100101
this.pathCache.set(path, pathArray)
101102
}
102103

103-
const length = pathArray.length
104104
let current = obj
105-
for (let i = 0; i < length; i++) {
105+
for (const part of pathArray) {
106106
if (current == null)
107107
return undefined
108-
current = current[pathArray[i]]
108+
current = current[part]
109109
}
110110
return current
111111
}
112112

113-
private createCharMask(text: string): Map<string, bigint> {
114-
return Effect.try({
115-
try: () => {
116-
const maskMap = new Map<string, bigint>()
117-
for (let i = 0; i < text.length; i++) {
118-
const char = text[i]
119-
maskMap.set(char, (maskMap.get(char) || 0n) | (1n << BigInt(i)))
120-
}
121-
return maskMap
122-
},
123-
catch: error => new Error(`[createCharMask]: Failed to create char mask: ${error}`),
124-
}).pipe(Effect.runSync)
113+
private createCharMask(text: string): Map<string, number[]> {
114+
const maskMap = new Map<string, number[]>()
115+
const chunkCount = Math.ceil(text.length / 32)
116+
117+
for (let i = 0; i < text.length; i++) {
118+
const char = text[i]
119+
const chunkIndex = Math.floor(i / 32)
120+
const bitPosition = i % 32
121+
122+
// eslint-disable-next-line unicorn/no-new-array
123+
let chunks = maskMap.get(char) || new Array(chunkCount).fill(0)
124+
if (chunks.length < chunkCount) {
125+
chunks = [...chunks, ...Array.from({ length: chunkCount - chunks.length }).fill(0)]
126+
}
127+
128+
chunks[chunkIndex] |= 1 << bitPosition
129+
maskMap.set(char, chunks)
130+
}
131+
132+
for (const [char, chunks] of maskMap) {
133+
if (chunks.length < chunkCount) {
134+
maskMap.set(char, [...chunks, ...Array.from<number>({ length: chunkCount - chunks.length }).fill(0)])
135+
}
136+
}
137+
138+
return maskMap
139+
}
140+
141+
private createInitialVP(m: number): number[] {
142+
const chunkCount = Math.ceil(m / 32)
143+
// eslint-disable-next-line unicorn/no-new-array
144+
const vp = new Array(chunkCount).fill(0)
145+
for (let i = 0; i < m; i++) {
146+
const chunkIndex = Math.floor(i / 32)
147+
const bitPosition = i % 32
148+
vp[chunkIndex] |= 1 << bitPosition
149+
}
150+
return vp
125151
}
126152

127153
private calculateMatchQuality(
128-
VP: bigint,
154+
VP: number[],
129155
m: number,
130156
currentIndex: number,
131157
normalizedText: string,
132158
query: string,
133159
): number {
134-
return Effect.try({
135-
try: () => {
136-
// Count the number of set bits (matches) in VP
137-
let matchCount = 0
138-
let mask = VP
139-
140-
while (mask !== 0n) {
141-
if ((mask & 1n) === 1n) {
142-
matchCount++
143-
}
144-
mask >>= 1n
145-
}
146-
147-
// Calculate the match ratio (matches / query length)
148-
const matchRatio = matchCount / m
149-
150-
// Penalize matches that are not contiguous or not aligned properly
151-
const isContiguous = this.isContiguousMatch(VP, m)
152-
const alignmentPenalty = isContiguous ? 1 : 0.2 // Reduce score for non-contiguous matches
153-
154-
// Calculate position bonus (matches at the start of the text score higher)
155-
const positionBonus = matchRatio > 0.5 ? (normalizedText.length - currentIndex) / normalizedText.length : 0
156-
157-
// Calculate partial match bonus (reward partial matches)
158-
const partialMatchBonus = normalizedText.includes(query) ? 1 : 0
159-
160-
// Combine factors to calculate score
161-
const score = (
162-
(matchRatio * 0.6) // 60% weight
163-
+ (alignmentPenalty * 0.3) // 30% weight
164-
+ (positionBonus * 0.05) // 5% weight
165-
+ (partialMatchBonus * 0.05) // 5% weight
166-
)
167-
168-
return Math.min(score, 1) // Ensure score does not exceed 1
169-
},
170-
catch: error => new Error(`[calculateMatchQuality]: Failed to calculate match quality: ${error}`),
171-
}).pipe(Effect.runSync)
160+
const matchCount = countSetBits(VP)
161+
const matchRatio = matchCount / m
162+
const isContiguous = this.isContiguousMatch(VP, m)
163+
const alignmentPenalty = isContiguous ? 1 : 0.2
164+
const positionBonus = matchRatio > 0.5 ? (normalizedText.length - currentIndex) / normalizedText.length : 0
165+
const partialMatchBonus = normalizedText.includes(query) ? 1 : 0
166+
167+
const score = (
168+
(matchRatio * 0.6)
169+
+ (alignmentPenalty * 0.3)
170+
+ (positionBonus * 0.05)
171+
+ (partialMatchBonus * 0.05)
172+
)
173+
174+
return Math.min(score, 1)
172175
}
173176

174-
private isContiguousMatch(VP: bigint, m: number): boolean {
175-
return Effect.try({
176-
try: () => {
177-
let first = -1
178-
let last = -1
179-
for (let i = 0; i < m; i++) {
180-
if ((VP & (1n << BigInt(i))) !== 0n) {
181-
if (first === -1)
182-
first = i
183-
last = i
184-
}
185-
}
186-
return (last - first + 1) === m
187-
},
188-
catch: error => new Error(`[isContiguousMatch]: Failed to check if match is contiguous: ${error}`),
189-
}).pipe(Effect.runSync)
177+
private isContiguousMatch(VP: number[], m: number): boolean {
178+
let first = -1
179+
let last = -1
180+
for (let i = 0; i < m; i++) {
181+
const chunkIndex = Math.floor(i / 32)
182+
const bitPosition = i % 32
183+
if (chunkIndex >= VP.length)
184+
break
185+
if ((VP[chunkIndex] & (1 << bitPosition)) !== 0) {
186+
if (first === -1)
187+
first = i
188+
last = i
189+
}
190+
}
191+
return (last - first + 1) === m && first !== -1
190192
}
191193

192-
private findMatchRanges(VP: bigint, m: number, endIndex: number): [number, number][] {
193-
return Effect.try({
194-
try: () => {
195-
const ranges: [number, number][] = []
196-
let start = -1
197-
let end = -1
198-
199-
for (let i = 0; i < m; i++) {
200-
if ((VP & (1n << BigInt(i))) !== 0n) {
201-
if (start === -1)
202-
start = endIndex - i
203-
end = endIndex - i
204-
}
205-
else if (start !== -1) {
206-
ranges.push([start, end])
207-
start = -1
208-
end = -1
209-
}
210-
}
194+
private findMatchRanges(VP: number[], m: number, endIndex: number): [number, number][] {
195+
const ranges: [number, number][] = []
196+
let start = -1
197+
let end = -1
198+
199+
for (let i = 0; i < m; i++) {
200+
const chunkIndex = Math.floor(i / 32)
201+
const bitPosition = i % 32
202+
if (chunkIndex < VP.length && (VP[chunkIndex] & (1 << bitPosition)) !== 0) {
203+
if (start === -1)
204+
start = endIndex - i
205+
end = endIndex - i
206+
}
207+
else if (start !== -1) {
208+
ranges.push([start, end])
209+
start = -1
210+
end = -1
211+
}
212+
}
211213

212-
if (start !== -1) {
213-
ranges.push([start, end])
214-
}
214+
if (start !== -1) {
215+
ranges.push([start, end])
216+
}
215217

216-
return ranges
217-
},
218-
catch: error => new Error(`[findMatchRanges]: Failed to find match ranges: ${error}`),
219-
}).pipe(Effect.runSync)
218+
return ranges
220219
}
221220
}

src/utils/index.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,98 @@ export function normalizeText(text: string): string {
1212
catch: error => new Error(`[normalizeText]: Failed to normalize text: ${error}`),
1313
}).pipe(Effect.runSync)
1414
}
15+
16+
export function bitwiseOR(a: number[], b: number[]): number[] {
17+
const result: number[] = []
18+
const maxLength = Math.max(a.length, b.length)
19+
for (let i = 0; i < maxLength; i++) {
20+
const aVal = i < a.length ? a[i] : 0
21+
const bVal = i < b.length ? b[i] : 0
22+
result.push(aVal | bVal)
23+
}
24+
return result
25+
}
26+
27+
export function bitwiseAND(a: number[], b: number[]): number[] {
28+
const result: number[] = []
29+
const maxLength = Math.max(a.length, b.length)
30+
for (let i = 0; i < maxLength; i++) {
31+
const aVal = i < a.length ? a[i] : 0
32+
const bVal = i < b.length ? b[i] : 0
33+
result.push(aVal & bVal)
34+
}
35+
return result
36+
}
37+
38+
export function bitwiseXOR(a: number[], b: number[]): number[] {
39+
const result: number[] = []
40+
const maxLength = Math.max(a.length, b.length)
41+
for (let i = 0; i < maxLength; i++) {
42+
const aVal = i < a.length ? a[i] : 0
43+
const bVal = i < b.length ? b[i] : 0
44+
result.push(aVal ^ bVal)
45+
}
46+
return result
47+
}
48+
49+
export const bitwiseNOT = (a: number[]): number[] => a.map(chunk => (~chunk) >>> 0)
50+
51+
export function bitwiseShiftLeft(a: number[]): number[] {
52+
const result: number[] = []
53+
let carry = 0
54+
for (const chunk of a) {
55+
const newChunk = (chunk << 1) | carry
56+
carry = (chunk >>> 31) & 1
57+
result.push(newChunk >>> 0)
58+
}
59+
if (carry)
60+
result.push(carry)
61+
return result
62+
}
63+
64+
export function addBitVectors(a: number[], b: number[]): number[] {
65+
const result: number[] = []
66+
let carry = 0
67+
const maxLength = Math.max(a.length, b.length)
68+
for (let i = 0; i < maxLength; i++) {
69+
const aVal = i < a.length ? a[i] : 0
70+
const bVal = i < b.length ? b[i] : 0
71+
const sum = aVal + bVal + carry
72+
carry = sum > 0xFFFFFFFF ? 1 : 0
73+
result.push(sum & 0xFFFFFFFF)
74+
}
75+
if (carry)
76+
result.push(carry)
77+
return result
78+
}
79+
80+
export function setLeastSignificantBit(a: number[]): number[] {
81+
if (a.length === 0)
82+
return [1]
83+
const newA = [...a]
84+
newA[0] |= 1
85+
return newA
86+
}
87+
88+
export function maskVP(vp: number[], m: number): number[] {
89+
const chunkCount = Math.ceil(m / 32)
90+
const masked = vp.slice(0, chunkCount)
91+
const lastChunkBits = m % 32 || 32
92+
if (masked.length > 0) {
93+
const lastIndex = masked.length - 1
94+
masked[lastIndex] &= (1 << lastChunkBits) - 1
95+
}
96+
return masked
97+
}
98+
99+
export function countSetBits(vp: number[]): number {
100+
let count = 0
101+
for (const chunk of vp) {
102+
let c = chunk
103+
while (c) {
104+
count += c & 1
105+
c >>>= 1
106+
}
107+
}
108+
return count
109+
}

0 commit comments

Comments
 (0)