RapidFuzz
Loading...
Searching...
No Matches
Levenshtein.hpp
1/* SPDX-License-Identifier: MIT */
2/* Copyright © 2022-present Max Bachmann */
3
4#pragma once
5#include <limits>
6#include <rapidfuzz/details/Range.hpp>
7#include <rapidfuzz/distance/Levenshtein_impl.hpp>
8
9namespace rapidfuzz {
10
145template <typename InputIt1, typename InputIt2>
146size_t levenshtein_distance(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2,
147 LevenshteinWeightTable weights = {1, 1, 1},
148 size_t score_cutoff = std::numeric_limits<size_t>::max(),
149 size_t score_hint = std::numeric_limits<size_t>::max())
150{
151 return detail::Levenshtein::distance(first1, last1, first2, last2, weights, score_cutoff, score_hint);
152}
153
154template <typename Sentence1, typename Sentence2>
155size_t levenshtein_distance(const Sentence1& s1, const Sentence2& s2,
156 LevenshteinWeightTable weights = {1, 1, 1},
157 size_t score_cutoff = std::numeric_limits<size_t>::max(),
158 size_t score_hint = std::numeric_limits<size_t>::max())
159{
160 return detail::Levenshtein::distance(s1, s2, weights, score_cutoff, score_hint);
161}
162
163template <typename InputIt1, typename InputIt2>
164size_t levenshtein_similarity(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2,
165 LevenshteinWeightTable weights = {1, 1, 1}, size_t score_cutoff = 0,
166 size_t score_hint = 0)
167{
168 return detail::Levenshtein::similarity(first1, last1, first2, last2, weights, score_cutoff, score_hint);
169}
170
171template <typename Sentence1, typename Sentence2>
172size_t levenshtein_similarity(const Sentence1& s1, const Sentence2& s2,
173 LevenshteinWeightTable weights = {1, 1, 1}, size_t score_cutoff = 0,
174 size_t score_hint = 0)
175{
176 return detail::Levenshtein::similarity(s1, s2, weights, score_cutoff, score_hint);
177}
178
179template <typename InputIt1, typename InputIt2>
180double levenshtein_normalized_distance(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2,
181 LevenshteinWeightTable weights = {1, 1, 1}, double score_cutoff = 1.0,
182 double score_hint = 1.0)
183{
184 return detail::Levenshtein::normalized_distance(first1, last1, first2, last2, weights, score_cutoff,
185 score_hint);
186}
187
188template <typename Sentence1, typename Sentence2>
189double levenshtein_normalized_distance(const Sentence1& s1, const Sentence2& s2,
190 LevenshteinWeightTable weights = {1, 1, 1}, double score_cutoff = 1.0,
191 double score_hint = 1.0)
192{
193 return detail::Levenshtein::normalized_distance(s1, s2, weights, score_cutoff, score_hint);
194}
195
255template <typename InputIt1, typename InputIt2>
256double levenshtein_normalized_similarity(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2,
257 LevenshteinWeightTable weights = {1, 1, 1},
258 double score_cutoff = 0.0, double score_hint = 0.0)
259{
260 return detail::Levenshtein::normalized_similarity(first1, last1, first2, last2, weights, score_cutoff,
261 score_hint);
262}
263
264template <typename Sentence1, typename Sentence2>
265double levenshtein_normalized_similarity(const Sentence1& s1, const Sentence2& s2,
266 LevenshteinWeightTable weights = {1, 1, 1},
267 double score_cutoff = 0.0, double score_hint = 0.0)
268{
269 return detail::Levenshtein::normalized_similarity(s1, s2, weights, score_cutoff, score_hint);
270}
271
287template <typename InputIt1, typename InputIt2>
288Editops levenshtein_editops(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2,
289 size_t score_hint = std::numeric_limits<size_t>::max())
290{
291 return detail::levenshtein_editops(detail::make_range(first1, last1), detail::make_range(first2, last2),
292 score_hint);
293}
294
295template <typename Sentence1, typename Sentence2>
296Editops levenshtein_editops(const Sentence1& s1, const Sentence2& s2,
297 size_t score_hint = std::numeric_limits<size_t>::max())
298{
299 return detail::levenshtein_editops(detail::make_range(s1), detail::make_range(s2), score_hint);
300}
301
302#ifdef RAPIDFUZZ_SIMD
303namespace experimental {
304template <int MaxLen>
305struct MultiLevenshtein : public detail::MultiDistanceBase<MultiLevenshtein<MaxLen>, size_t, 0,
306 std::numeric_limits<int64_t>::max()> {
307private:
308 friend detail::MultiDistanceBase<MultiLevenshtein<MaxLen>, size_t, 0,
309 std::numeric_limits<int64_t>::max()>;
310 friend detail::MultiNormalizedMetricBase<MultiLevenshtein<MaxLen>, size_t>;
311
312 RAPIDFUZZ_CONSTEXPR_CXX14 static size_t get_vec_size()
313 {
314# ifdef RAPIDFUZZ_AVX2
315 using namespace detail::simd_avx2;
316# else
317 using namespace detail::simd_sse2;
318# endif
319 RAPIDFUZZ_IF_CONSTEXPR (MaxLen <= 8)
320 return native_simd<uint8_t>::size;
321 else RAPIDFUZZ_IF_CONSTEXPR (MaxLen <= 16)
322 return native_simd<uint16_t>::size;
323 else RAPIDFUZZ_IF_CONSTEXPR (MaxLen <= 32)
324 return native_simd<uint32_t>::size;
325 else RAPIDFUZZ_IF_CONSTEXPR (MaxLen <= 64)
326 return native_simd<uint64_t>::size;
327
328 static_assert(MaxLen <= 64, "expected MaxLen <= 64");
329 }
330
331 static size_t find_block_count(size_t count)
332 {
333 size_t vec_size = get_vec_size();
334 size_t simd_vec_count = detail::ceil_div(count, vec_size);
335 return detail::ceil_div(simd_vec_count * vec_size * MaxLen, 64);
336 }
337
338public:
339 MultiLevenshtein(size_t count, LevenshteinWeightTable aWeights = {1, 1, 1})
340 : input_count(count), PM(find_block_count(count) * 64), weights(aWeights)
341 {
342 str_lens.resize(result_count());
343 if (weights.delete_cost != 1 || weights.insert_cost != 1 || weights.replace_cost > 2)
344 throw std::invalid_argument("unsupported weights");
345 }
346
356 size_t result_count() const
357 {
358 size_t vec_size = get_vec_size();
359 size_t simd_vec_count = detail::ceil_div(input_count, vec_size);
360 return simd_vec_count * vec_size;
361 }
362
363 template <typename Sentence1>
364 void insert(const Sentence1& s1_)
365 {
366 insert(detail::to_begin(s1_), detail::to_end(s1_));
367 }
368
369 template <typename InputIt1>
370 void insert(InputIt1 first1, InputIt1 last1)
371 {
372 auto len = std::distance(first1, last1);
373 int block_pos = static_cast<int>((pos * MaxLen) % 64);
374 auto block = (pos * MaxLen) / 64;
375 assert(len <= MaxLen);
376
377 if (pos >= input_count) throw std::invalid_argument("out of bounds insert");
378
379 str_lens[pos] = static_cast<size_t>(len);
380 for (; first1 != last1; ++first1) {
381 PM.insert(block, *first1, block_pos);
382 block_pos++;
383 }
384 pos++;
385 }
386
387private:
388 template <typename InputIt2>
389 void _distance(size_t* scores, size_t score_count, const detail::Range<InputIt2>& s2,
390 size_t score_cutoff = std::numeric_limits<size_t>::max()) const
391 {
392 if (score_count < result_count())
393 throw std::invalid_argument("scores has to have >= result_count() elements");
394
395 auto scores_ = detail::make_range(scores, scores + score_count);
396 RAPIDFUZZ_IF_CONSTEXPR (MaxLen == 8)
397 detail::levenshtein_hyrroe2003_simd<uint8_t>(scores_, PM, str_lens, s2, score_cutoff);
398 else RAPIDFUZZ_IF_CONSTEXPR (MaxLen == 16)
399 detail::levenshtein_hyrroe2003_simd<uint16_t>(scores_, PM, str_lens, s2, score_cutoff);
400 else RAPIDFUZZ_IF_CONSTEXPR (MaxLen == 32)
401 detail::levenshtein_hyrroe2003_simd<uint32_t>(scores_, PM, str_lens, s2, score_cutoff);
402 else RAPIDFUZZ_IF_CONSTEXPR (MaxLen == 64)
403 detail::levenshtein_hyrroe2003_simd<uint64_t>(scores_, PM, str_lens, s2, score_cutoff);
404 }
405
406 template <typename InputIt2>
407 size_t maximum(size_t s1_idx, const detail::Range<InputIt2>& s2) const
408 {
409 return detail::levenshtein_maximum(str_lens[s1_idx], s2.size(), weights);
410 }
411
412 size_t get_input_count() const noexcept
413 {
414 return input_count;
415 }
416
417 size_t input_count;
418 size_t pos = 0;
419 detail::BlockPatternMatchVector PM;
420 std::vector<size_t> str_lens;
421 LevenshteinWeightTable weights;
422};
423} /* namespace experimental */
424#endif /* RAPIDFUZZ_SIMD */
425
426template <typename CharT1>
427struct CachedLevenshtein : public detail::CachedDistanceBase<CachedLevenshtein<CharT1>, size_t, 0,
428 std::numeric_limits<int64_t>::max()> {
429 template <typename Sentence1>
430 explicit CachedLevenshtein(const Sentence1& s1_, LevenshteinWeightTable aWeights = {1, 1, 1})
431 : CachedLevenshtein(detail::to_begin(s1_), detail::to_end(s1_), aWeights)
432 {}
433
434 template <typename InputIt1>
435 CachedLevenshtein(InputIt1 first1, InputIt1 last1, LevenshteinWeightTable aWeights = {1, 1, 1})
436 : s1(first1, last1), PM(detail::make_range(first1, last1)), weights(aWeights)
437 {}
438
439private:
440 friend detail::CachedDistanceBase<CachedLevenshtein<CharT1>, size_t, 0,
441 std::numeric_limits<int64_t>::max()>;
442 friend detail::CachedNormalizedMetricBase<CachedLevenshtein<CharT1>>;
443
444 template <typename InputIt2>
445 size_t maximum(const detail::Range<InputIt2>& s2) const
446 {
447 return detail::levenshtein_maximum(s1.size(), s2.size(), weights);
448 }
449
450 template <typename InputIt2>
451 size_t _distance(const detail::Range<InputIt2>& s2, size_t score_cutoff, size_t score_hint) const
452 {
453 if (weights.insert_cost == weights.delete_cost) {
454 /* when insertions + deletions operations are free there can not be any edit distance */
455 if (weights.insert_cost == 0) return 0;
456
457 /* uniform Levenshtein multiplied with the common factor */
458 if (weights.insert_cost == weights.replace_cost) {
459 // max can make use of the common divisor of the three weights
460 size_t new_score_cutoff = detail::ceil_div(score_cutoff, weights.insert_cost);
461 size_t new_score_hint = detail::ceil_div(score_hint, weights.insert_cost);
462 size_t dist = detail::uniform_levenshtein_distance(PM, detail::make_range(s1), s2,
463 new_score_cutoff, new_score_hint);
464 dist *= weights.insert_cost;
465
466 return (dist <= score_cutoff) ? dist : score_cutoff + 1;
467 }
468 /*
469 * when replace_cost >= insert_cost + delete_cost no substitutions are performed
470 * therefore this can be implemented as InDel distance multiplied with the common factor
471 */
472 else if (weights.replace_cost >= weights.insert_cost + weights.delete_cost) {
473 // max can make use of the common divisor of the three weights
474 size_t new_max = detail::ceil_div(score_cutoff, weights.insert_cost);
475 size_t dist = detail::indel_distance(PM, detail::make_range(s1), s2, new_max);
476 dist *= weights.insert_cost;
477 return (dist <= score_cutoff) ? dist : score_cutoff + 1;
478 }
479 }
480
481 return detail::generalized_levenshtein_distance(detail::make_range(s1), s2, weights, score_cutoff);
482 }
483
484 std::vector<CharT1> s1;
485 detail::BlockPatternMatchVector PM;
486 LevenshteinWeightTable weights;
487};
488
489#ifdef RAPIDFUZZ_DEDUCTION_GUIDES
490template <typename Sentence1>
491explicit CachedLevenshtein(const Sentence1& s1_, LevenshteinWeightTable aWeights = {
492 1, 1, 1}) -> CachedLevenshtein<char_type<Sentence1>>;
493
494template <typename InputIt1>
495CachedLevenshtein(InputIt1 first1, InputIt1 last1,
496 LevenshteinWeightTable aWeights = {1, 1, 1}) -> CachedLevenshtein<iter_value_t<InputIt1>>;
497#endif
498
501} // namespace rapidfuzz
Editops levenshtein_editops(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, size_t score_hint=std::numeric_limits< size_t >::max())
Return list of EditOp describing how to turn s1 into s2.
Definition Levenshtein.hpp:288
size_t levenshtein_distance(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, LevenshteinWeightTable weights={1, 1, 1}, size_t score_cutoff=std::numeric_limits< size_t >::max(), size_t score_hint=std::numeric_limits< size_t >::max())
Calculates the minimum number of insertions, deletions, and substitutions required to change one sequ...
Definition Levenshtein.hpp:146
double levenshtein_normalized_similarity(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, LevenshteinWeightTable weights={1, 1, 1}, double score_cutoff=0.0, double score_hint=0.0)
Calculates a normalized levenshtein distance using custom costs for insertion, deletion and substitut...
Definition Levenshtein.hpp:256