Text::Fuzzy(3)

๐Ÿ“ฆ
version 0.29 ยท 3 versions
, in epel 9

โœจ NAME โœจ

okay so bestie, welcome to Text::Fuzzy โ€” the 3 manual page for partial string matching using edit distances. yes, that's the whole vibe: fuzzy matching, but make it fashion ๐Ÿ’…


๐ŸŒธ SYNOPSIS ๐ŸŒธ

okay so first, the flex:

use Text::Fuzzy;
my $tf = Text::Fuzzy->new ('boboon');
print "Distance is ", $tf->distance ('babboon'), "\n";

my @words = qw/the quick brown fox jumped over the lazy dog/;
my $nearest = $tf->nearestv (\@words);
print "Nearest array entry is $nearest\n";

and that little script produces:

Distance is 2
Nearest array entry is brown

This example is included as synopsis.pl (https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/synopsis.pl) in the distribution. ๐Ÿซถ


๐ŸŽ€ VERSION ๐ŸŽ€

This documents version 0.29 of Text::Fuzzy corresponding to git commit 0e5db1fbccbc7d7518ec39a95d43a5b20166a727 (https://github.com/benkasminbullock/Text-Fuzzy/commit/0e5db1fbccbc7d7518ec39a95d43a5b20166a727) released on Thu Dec 10 14:38:31 2020 +0900. yes, that's the exact receipt, bestie ๐Ÿงพโœจ


๐Ÿฆ‹ DESCRIPTION ๐Ÿฆ‹

This module calculates edit distances between words, and searches arrays and files to find the nearest entry by edit distance. It handles both byte strings and character strings (strings containing Unicode), treating each Unicode character as a single entity. iconic behavior, honestly ๐Ÿ‘‘

use Text::Fuzzy;
use utf8;
my $tf = Text::Fuzzy->new ('ใ‚ใ„ใ†ใˆใŠโ˜บ');
print $tf->distance ('ใ†ใˆใŠโ˜บ'), "\n";

produces output:

2

This example is included as unicode.pl (https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/unicode.pl) in the distribution. ๐ŸŒˆ

The default edit distance is the Levenshtein one, which counts each addition ("cat" -> "cart"), substitution ("cat" -> "cut"), and deletion ("carp" -> "cap") as one unit. The Damerau-Levenshtein edit distance, which also allows transpositions ("salt" -> "slat") may also be selected with the transpositions_ok method or the trans option. periodt ๐Ÿ’…

This module is particularly suited to searching for the nearest match to a term over a list of words, using the nearestv or nearest methods. It studies the target string to be matched (the first argument to new) to build information to rapidly reject mismatches in a list. Since computing the Levenshtein and Damerau-Levenshtein edit distances with the Wagner-Fischer algorithm is computationally expensive, the module offers a boost in performance for searching for a string in a list of words. no cap, that's the whole point ๐Ÿš€


๐Ÿ‘‘ METHODS ๐Ÿ‘‘

๐Ÿ’… new

my $tf = Text::Fuzzy->new ('bibbety bobbety boo');

Create a new Text::Fuzzy object from the supplied word. cute, simple, main character energy โœจ

The following parameters may be supplied to new:

๐ŸŒˆ max

my $tf = Text::Fuzzy->new ('Cinderella', max => 3);

This option affects the behaviour of nearestv and nearest methods. When searching over an array, this sets the maximum edit distance allowed for a word to be considered a "near match". For example, with

my $tf = Text::Fuzzy->new ('Cinderella');
$tf->set_max_distance (3);

when using nearest, Cinder will not be considered a match, but derella will. the way that works is lowkey iconic ๐ŸŽ€

To switch off the maximum distance, and allow all words to be considered, you can set max to be a negative value:

my $tf = Text::Fuzzy->new ('Cinderella', max => -1);

Note that this is the default, so there is hardly any point specifying it, except if you want to make self-documenting code, or you're worried that the module's default behaviour may suddenly change. fair, honestly ๐Ÿ™ˆ

Setting max to zero makes $tf only match exactly.

The method set_max_distance does the same thing as this parameter.

๐Ÿ“ no_exact

my $tf = Text::Fuzzy->new ('slipper', no_exact => 1);

This parameter switches on rejection of exact matches, in the same way as the method no_exact:

my $tf = Text::Fuzzy->new ('slipper');
$tf->no_exact (1);

This is useful for the case of scanning an array which contains the search term itself, when we are interested in near matches only. For example, if we have a dictionary of words and we need to find near matches for a word which is in the dictionary. yes bestie, we're rejecting the exact clone and looking for the vibes nearby ๐Ÿ’€

๐Ÿฆ‹ trans

my $tf = Text::Fuzzy->new ('glass', trans => 1);

This switches on transpositions, in other words it uses the Damerau-Levenshtein edit distance rather than the Levenshtein edit distance. The method transpositions_ok has the same effect as this. slay, swap those letters ๐Ÿ”


๐Ÿ’– distance

my $dist = $tf->distance ($word);

This method's return value is the edit distance to $word from the word used to create the object in new.

use Text::Fuzzy;
my $cat = Text::Fuzzy->new ('cat');
print $cat->distance ('cut'), "\n";
print $cat->distance ('cart'), "\n";
print $cat->distance ('catamaran'), "\n";
use utf8;
print $cat->distance ('ฮณแฝฑฯ„ฮฟฯ‚'), "\n";

produces output:

1
1
6
5

This example is included as distance.pl (https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/distance.pl) in the distribution. ๐Ÿฑโœจ

To know which edits are used to convert the words, use distance_edits.


๐ŸŒŸ nearestv

my $nearest_word = $tf->nearestv (\@words);
my @nearest_words = $tf->nearestv (\@words);

Returns the value in @words which has the nearest distance to the value given to $tf in new. In array context, it returns a list of the nearest values. bestie, it's giving "find your people" energy ๐Ÿซถ

use Text::Fuzzy;
my @words = (qw/who where what when why/);
my $tf = Text::Fuzzy->new ('whammo');
my @nearest = $tf->nearestv (\@words);
print "@nearest\n";

produces output:

who what

This example is included as nearestv.pl (https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/nearestv.pl) in the distribution. ๐ŸŒธ

The behaviour of the match can be controlled with no_exact and set_max_distance in exactly the same way as nearest.

This is a convenient wrapper around the nearest function. nearest is annoying to use, because it only returns array offsets, and also error-prone due to having to check to distinguish the first element of the array from an undefined value using defined. real talk, that's a whole lot of extra stress ๐Ÿ˜ญ

This method was added in version 0.18 of Text::Fuzzy.


๐Ÿ’œ nearest

my $index = $tf->nearest (\@words);
my $nearest_word = $words[$index];

Given an array reference, this returns a number, the index of the nearest element in the array @words to the argument to new. Having found the nearest match you then need to look up the value in the array, as in $nearest_word above. yes, it hands you the address, not the whole gift ๐ŸŽ

It is possible to set a maximum edit distance, beyond which entries are rejected, using set_max_distance or the max parameter to new. In this case, if none of the elements of @words are less than the maximum distance away from the word, $index is the undefined value, so when setting a maximum distance, it is necessary to check the return value of index using defined. do not skip that check, bestie, that's the danger zone ๐Ÿ’€

use Text::Fuzzy;
my $tf = Text::Fuzzy->new ('calamari', max => 1);
my @words = qw/Have you ever kissed in the moonlight
               In the grand and glorious
               Gay notorious
               South American Way?/;
my $index = $tf->nearest (\@words);
if (defined $index) {
    printf "Found at $index, distance was %d.\n",
    $tf->last_distance ();
}
else {
    print "Not found anywhere.\n";
}

produces output:

Not found anywhere.

This example is included as check-return.pl (https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/check-return.pl) in the distribution. ๐Ÿ˜ญ

If there is more than one word with the same edit distance in @words, this returns the last one found, unless it is an exact match, in which case it returns the first one found. To get all matches, call it in array context:

my @nearest = $tf->nearest (\@words);

In array context, if there are no matches within the minimum distance, nearest returns an empty list. If there is one or more match, it returns the array offset of it or them, not the value itself. offsets only, no values, that's the rule ๐Ÿ’…

use Text::Fuzzy;

my @funky_words = qw/nice funky rice gibbon lice graeme garden/;
my $tf = Text::Fuzzy->new ('dice');
my @nearest = $tf->nearest (\@funky_words);

print "The nearest words are ";
print join ", ", (map {$funky_words[$_]} @nearest);
printf ", distance %d.\n", $tf->last_distance ();

produces output:

The nearest words are nice, rice, lice, distance 1.

This example is included as list-context.pl (https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/list-context.pl) in the distribution. ๐Ÿ“


๐Ÿงš last_distance

my $last_distance = $tf->last_distance ();

The distance from the previous match's closest match. This is used in conjunction with nearest or nearestv to find the edit distance to the previous match. it's the receipts from the last search, basically ๐Ÿงพโœจ

use Text::Fuzzy;
my @words = (qw/who where what when why/);
my $tf = Text::Fuzzy->new ('whammo');
my @nearest = $tf->nearestv (\@words);
print "@nearest\n";
print $tf->last_distance (), "\n";
# Prints 3, the number of edits needed to turn "whammo" into "who"
# (delete a, m, m) or into "what" (replace m with t, delete m, delete
# o).

produces output:

who what
3

This example is included as last-distance.pl (https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/last-distance.pl) in the distribution. ๐ŸŒŸ


๐Ÿ”ฅ set_max_distance

# Set the max distance.
$tf->set_max_distance (3);

Set the maximum edit distance of $tf. Set the maximum distance to a low value to improve the speed of searches over lists with nearest, or to reject unlikely matches. When searching for a near match, anything with an edit distance of a value over the maximum is rejected without computing the exact distance. To compute exact distances, call this method without an argument:

$tf->set_max_distance ();

The maximum edit distance is switched off, and whatever the nearest match is is accepted. A negative value also switches it off:

$tf->set_max_distance (-1);

The object created by new has no maximum distance unless specified by the user. default behavior, no extra limits, periodt ๐Ÿ’…

use Text::Fuzzy;
my $tf = Text::Fuzzy->new ('nopqrstuvwxyz');
# Prints 13, the correct value.
print $tf->distance ('abcdefghijklm'), "\n";
$tf->set_max_distance (10);
# Prints 11, one more than the maximum distance, because the search
# stopped when the distance was exceeded.
print $tf->distance ('abcdefghijklm'), "\n";

produces output:

13
11

(This example ships with the dist as max-dist.pl โ€” peep it at <https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/max-dist.pl> ๐Ÿ’–)

Setting the max distance is lowkey a speed hack, okay? Say you're searching through a dictionary of 100,000 words, or a million โ€” and you only care about close matches โ€” setting the max distance to something small lets the computer yeet the bad candidates way faster. Calculating Levenshtein distance is an O(n^2) algorithm in the lengths of the words, so even a tiny bump in the maximum allowed distance means a much bigger pile of work for the poor CPU. But with a maximum distance set, it can give up on hopeless matches real quick. Efficiency slay โœจ

๐ŸŒ€ transpositions_ok

$tf->transpositions_ok (1);

Drop a true value in the arg and you change which flavor of edit distance gets used โ€” now transpositions are allowed, so "clam" and "calm" can count as close. Out of the box transpositions are not allowed, which gives you the Levenshtein edit distance. Turn transpositions on and it becomes the Damerau-Levenshtein edit distance. A false value shuts that down:

$tf->transpositions_ok (0);

๐Ÿ™ˆ no_exact

$tf->no_exact (1);

Bestie this is a flag for "nearest" and what it does is make it ignore exact matches. Like, it just refuses to let a word match itself. Example time:

use Text::Fuzzy;

my @words = qw/bibbity bobbity boo/;
for my $word (@words) {
    my $tf = Text::Fuzzy->new ($word);
    $tf->no_exact (0);
    my $nearest1 = $tf->nearest (\@words);
    print "With exact, nearest to $word is $words[$nearest1]\n";
    # Make "$word" not match itself.
    $tf->no_exact (1);
    my $nearest2 = $tf->nearest (\@words);
    print "Without exact, nearest to $word is $words[$nearest2]\n";
}

and that produces:

With exact, nearest to bibbity is bibbity
Without exact, nearest to bibbity is bobbity
With exact, nearest to bobbity is bobbity
Without exact, nearest to bobbity is bibbity
With exact, nearest to boo is boo
Without exact, nearest to boo is bobbity

(This one's in the dist as no-exact.pl โ€” <https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/no-exact.pl> ๐ŸŽ€)

So basically it's for when you're searching over an array that contains the very thing you're searching for. Awkward. no_exact said "not today" ๐Ÿ’…

๐Ÿ“‚ scan_file

my $nearest = $tf->scan_file ('/usr/share/dict/words');

Scans a whole file to find the nearest match to the word you passed to "new". Heads up tho: this assumes the file is lines of text separated by newlines, and it hunts down the closest match in the file. Return value is a string, not a line number โ€” and ngl it can't return an array of values. Also it does not currently support Unicode-encoded files. ๐Ÿซถ


โญ FUNCTIONS

These ones are free agents โ€” no "Text::Fuzzy" object required, periodt.

๐Ÿงฎ distance_edits

my ($distance, $edits) = distance_edits ('before', 'after');

This hands you the edit distance between the two arguments, plus the edits you'd need to transform the first one into the second. $edits is a string made of four letters โ€” k, r, d, and i โ€” standing for "keep", "replace", "delete", and "insert" respectively. So like, for "piece" and "peace", $edits contains "krrkk" meaning "keep, replace, replace, keep, keep". Iconic.

use Text::Fuzzy 'distance_edits';
my @words = (qw/who where what when why/);
my $tf = Text::Fuzzy->new ('whammo');
my @nearest = $tf->nearestv (\@words);
print "@nearest\n";
# Prints "who what"
print $tf->last_distance (), "\n";
# Prints 3, the number of edits needed to turn "whammo" into "who"
# (delete a, m, m) or into "what" (replace m with t, delete m, delete
# o).
my ($distance, $edits) = distance_edits ('whammo', 'who');
print "$edits\n";
# Prints kkdddk, keep w, keep h, delete a, delete m, delete m, keep o.

which gives you:

who what
3
kkdddk

(That's distance-edits.pl in the dist โ€” <https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/distance-edits.pl> ๐ŸŒธ)

โš ๏ธ Quick reality check: this does not handle transpositions. And unlike basically the rest of the module, it's pure Perl rather than XS, and it is not optimized for speed. The edit distance search inside "nearest" is optimized for speed, and because of that it throws away its record of the edits it used to get the result. Trade-offs, bestie ๐Ÿ˜ญ

๐Ÿ” fuzzy_index

my ($offset, $edits, $distance) = fuzzy_index ($needle, $haystack);

Searches for $needle in $haystack using fuzzy matching. Very demure, very mindful.

The return value is the offset of the closest match it found, the edits necessary on $needle to make it into the matching text, and the Levenshtein edit distance between the matching part of $haystack and $needle. Three-for-one, we love to see it ๐Ÿ’œ

For the algorithm used, go look at:

<http://ginstrom.com/scribbles/2007/12/01/fuzzy-substring-matching-with-levenshtein-distance-in-python/>

๐Ÿ’€ Warning: this is implemented in Perl, not C, and it's slow because there's a ton of debugging code hanging around in there. Please expect both the interface and the internals to change. Consider yourself warned, bestie.

โœจ EXAMPLES โœจ

okay so this section is the fun part โ€” extended examples of the module doing actual real-world stuff. practical problems, solved, slayed ๐Ÿ’…


๐ŸŽ€ misspelt-web-page.cgi

the file examples/misspelt-web-page.cgi <https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/misspelt-web-page.cgi> is a CGI script that lowkey does the same thing as the Apache mod_speling module โ€” it offers spelling corrections for mistyped URLs and sends the user to a correct page. iconic behaviour, honestly.

use Text::Fuzzy;

# The directory of files served by the web server.

my $web_root = '/usr/local/www/data';

# If the query is "http://www.example.com/abc/xyz.html", $path_info is
# "abc/xyz.html".

my $path_info = $ENV{REQUEST_URI};

if (! defined $path_info) {
    fail ("No path info");
}

if ($0 =~ /$path_info/) {
    fail ("Don't redirect to self");
}

# This is the list of files under the main page.

my @allfiles = get_all_files ($web_root, '');

# This is our spelling search engine.

my $tf = Text::Fuzzy->new ($path_info);

my $nearest = $tf->nearest (\@allfiles, max => 5);

if (defined $nearest) {
    redirect ($allfiles[$nearest]);
}
else {
    fail ("Nothing like $path_info was found");
}
exit;

# Read all the files under "$root/$dir". This is recursive. The return
# value is an array containing all files found.

sub get_all_files
{
    my ($root, $dir) = @_;
    my @allfiles;
    my $full_dir = "$root/$dir";
    if (! -d $full_dir) {
        fail ("$full_dir is not a directory");
    }
    opendir DIR, $full_dir or fail ("Can't open directory $full_dir: $!");
    my @files = grep !/^\./, readdir DIR;
    closedir DIR or fail ("Can't close $full_dir: $!");
    for my $file (@files) {
        my $dir_file = "$dir/$file";
        my $full_file = "$root/$dir_file";
        if (-d $full_file) {
            push @allfiles, get_all_files ($root, $dir_file);
        }
        else {
            push @allfiles, $dir_file;
        }
    }
    return @allfiles;
}

# Print a "permanent redirect" to the respelt name, then exit.

sub redirect
{
    my ($url) = @_;
    print <<EOF;
Status: 301
Location: $url

EOF
    exit;
}

# Print an error message for the sake of the requester, and print a
# message to the error log, then exit.

sub fail
{
    my ($error) = @_;
    print <<EOF;
Content-Type: text/plain

$error
EOF
    # Add the name of the program and the time to the error message,
    # otherwise the error log will get awfully confusing-looking.
    my $time = scalar gmtime ();
    print STDERR "$0: $time: $error\n";
    exit;
}

see also <http://www.lemoda.net/perl/perl-mod-speling/> for how to set up .htaccess to use the script ๐Ÿ‘‘


๐ŸŽ€ spell-check.pl

the file examples/spell-check.pl <https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/spell-check.pl> is a spell checker, bestie. it uses a dictionary of words specified by a command-line option -d:

spell-check.pl -d /usr/dict/words file1.txt file2.txt

it prints out any words which look like spelling mistakes, using the dictionary. so helpful, so demure ๐ŸŽ€

use Getopt::Long;
use Text::Fuzzy;
use Lingua::EN::PluralToSingular 'to_singular';

# The location of the Unix dictionary.
my $dict = '/usr/share/dict/words';

# Default maximum edit distance. Five is quite a big number for a
# spelling mistake.
my $max = 5;

GetOptions (
    "dict=s" => \$dict,
    "max=i" => \$max,
);

my @words;
my %words;
my $min_length = 4;
read_dictionary ($dict, \@words, \%words);
# Known mistakes, don't repeat.
my %known;
# Spell-check each file on the command line.
for my $file (@ARGV) {
    open my $input, "<", $file or die "Can't open $file: $!";
    while (<$input>) {
        my @line = split /[^a-z']+/i, $_;
        for my $word (@line) {
            # Remove leading/trailing apostrophes.
            $word =~ s/^'|'$//g;
            my $clean_word = to_singular (lc $word);
            $clean_word =~ s/'s$//;
            if ($words{$clean_word} || $words{$word}) {
                # It is in the dictionary.
                next;
            }
            if (length $word < $min_length) {
                # Very short words are ignored.
                next;
            }
            if ($word eq uc $word) {
                # Acronym like BBC, IRA, etc.
                next;
            }
            if ($known{$clean_word}) {
                # This word was already given to the user.
                next;
            }
            if ($clean_word =~ /(.*)ed$/ || $clean_word =~ /(.*)ing/) {
                my $stem = $1;
                if ($words{$stem} || $words{"${stem}e"}) {
                    # Past/gerund of $stem/${stem}e
                    next;
                }
                # Test for doubled end consonants,
                # e.g. "submitted"/"submit".
                if ($stem =~ /([bcdfghjklmnpqrstvwxz])\1/) {
                    $stem =~ s/$1$//;
                    if ($words{$stem}) {
                        # Past/gerund of $stem/${stem}e
                        next;
                    }
                }
            }
            my $tf = Text::Fuzzy->new ($clean_word, max => $max);
            my $nearest = $tf->nearest (\@words);
            # We have set a maximum distance to search for, so we need
            # to check whether $nearest is defined.
            if (defined $nearest) {
                my $correction = $words[$nearest];
                print "$file:$.: '$word' may be '$correction'.\n";
                $known{$clean_word} = $correction;
            }
            else {
                print "$file:$.: $word may be a spelling mistake.\n";
                $known{$clean_word} = 1;
            }
        }
    }
    close $input or die $!;
}

exit;

sub read_dictionary
{
    my ($dict, $words_array, $words_hash) = @_;
    open my $din, "<", $dict or die "Can't open dictionary $dict: $!";
    my @words;
    while (<$din>) {
        chomp;
        push @words, $_;
    }
    close $din or die $!;
    # Apostrophe words

    my @apo = qw/

                    let's I'll you'll he'll she'll they'll we'll I'm
                    you're he's she's it's we're they're I've they've
                    you've we've one's isn't aren't doesn't don't
                    won't wouldn't I'd you'd he'd we'd they'd
                    shouldn't couldn't didn't can't

                /;

    # Irregular past participles.
    my @pp = qw/became/;

    push @words, @apo, @pp;
    for (@words) {
        push @$words_array, lc $_;
        $words_hash->{$_} = 1;
        $words_hash->{lc $_} = 1;
    }
}

ngl, because the usual Unix dictionary doesn't have plurals, it uses Lingua::EN::PluralToSingular to convert nouns into singular forms. unfortunately it still misses past participles and past tenses of verbs ๐Ÿ˜ญ rip.


๐ŸŽ€ extract-kana.pl

the file examples/extract-kana.pl <https://fastapi.metacpan.org/source/BKB/Text-Fuzzy-0.29/examples/extract-kana.pl> extracts the kana entries from edict โ€” a freely-available Japanese to English electronic dictionary โ€” and does some fuzzy searches on them โœจ it requires a local copy of the file to run. this script is literally the Unicode-searches demo for Text::Fuzzy, iykyk.

use Lingua::JA::Moji ':all';
use Text::Fuzzy;
use utf8;
my $infile = '/home/ben/data/edrdg/edict';
open my $in, "<:encoding(EUC-JP)", $infile or die $!;
my @kana;
while (<$in>) {
    my $kana;
    if (/\[(\p{InKana}+)\]/) {
        $kana = $1;
    }
    elsif (/^(\p{InKana}+)/) {
        $kana = $1;
    }
    if ($kana) {
        $kana = kana2katakana ($kana);
        push @kana, $kana;
    }
}
printf "Starting fuzzy searches over %d lines.\n", scalar @kana;
search ('ใ‚ฆใ‚ชใ‚ฝใ‚ฆใ‚ณ');
search ('ใ‚ขใ‚คใ‚ฆใ‚จใ‚ชใ‚ซใ‚ญใ‚ฏใ‚ฑใ‚ณใƒใƒ“ใƒ–ใƒ™ใƒœใƒใƒ’ใƒ•ใƒ˜ใƒ›');
search ('ใ‚ขใƒซใƒ™ใƒซใƒˆใ‚ขใ‚คใƒณใ‚ทใƒฅใ‚ฟใ‚คใƒณ');
search ('ใƒใƒใƒใƒ–');
search ('ใƒใƒใƒใƒ–ใ‚ขใƒซใƒ™ใƒซใƒˆ');
exit;

sub search
{
    my ($silly) = @_;
    my $max = 10;
    my $search = Text::Fuzzy->new ($silly, max => $max);
    my $n = $search->nearest (\@kana);
    if (defined $n) {
        printf "$silly nearest is $kana[$n] (distance %d)\n",
            $search->last_distance ();
    }
    else {
        printf "Nothing like '$silly' was found within $max edits.\n";
    }
}

๐ŸŽ€ Lingua::JA::Gairaigo::Fuzzy

the module Lingua::JA::Gairaigo::Fuzzy tries to determine whether two Japanese loanwords are the same word or not. cute little identity crisis, love that for them ๐Ÿฆ‹


๐ŸŽ€ CPAN::Nearest

the module CPAN::Nearest offers a search over the titles of CPAN modules using a fuzzy search to get the nearest match. basically a matchmaker for module names ๐Ÿ’–


โœจ DEPENDENCIES โœจ

this module has no dependencies on other modules. self-sufficient queen behaviour, no cap ๐Ÿ‘‘


๐Ÿ’– SUPPORT ๐Ÿ’–

Reporting a bug

there is a bug tracker for the module at <https://github.com/benkasminbullock/Text-Fuzzy/issues>. bestie, if something's giving broken, that's where you go ๐Ÿ›

Testing

the CPAN tester results are at <http://www.cpantesters.org/distro/T/Text-Fuzzy.html>. the ActiveState tester results are at <http://code.activestate.com/ppm/Text-Fuzzy/>. go check the vibes, everything's transparent here fr fr ๐ŸŒˆ

๐Ÿซฃ PRIVATE METHODS

okay so these next ones are labeled "private" โ€” they're mostly for benchmarking the module and checking that it actually works correctly. so like, internal bestie business, yk? ๐Ÿ’…

no_alphabet

$tf->no_alphabet (1);

this turns off alphabetizing of the string. so what even is alphabetizing? it's a filter where the intersection of all the characters in the two strings gets computed, and if the alphabetical difference of the two strings is greater than the maximum distance, the match gets rejected right there โ€” without ever applying the dynamic programming algorithm. and that's a speed win, because the dynamic programming algorithm is slow. ๐Ÿ˜ญ

the alphabetizing should never ever reject anything that's a legitimate match, and it should make the program run faster in almost every case. the only uses anyone can imagine for switching it off are (1) checking that the algorithm is working correctly and (2) benchmarking performance. so yeah, that's the whole vibe check. โœจ

get_trans

my $trans_ok = $tf->get_trans ();

this returns the value that was set by transpositions_ok. simple as that. ๐ŸŽ€

unicode_length

my $length = $tf->unicode_length ();

this returns the length in characters (not bytes, bestie, characters) of the string used in new. if the string isn't marked as Unicode, it returns the undefined value. in the snippet below, $l1 should be equal to $l2:

use utf8;
my $word = 'โ……โ…†โ…‡โ…ˆโ…‰';
my $l1 = length $word;
my $tf = Text::Fuzzy->new ($word);
my $l2 = $tf->unicode_length ();

look at that gorgeous Unicode word, she's iconic fr. ๐ŸŒˆ

ualphabet_rejections

my $rejected = $tf->ualphabet_rejections ();

after running nearest over an array, this hands you the number of entries in the array that got rejected using only the Unicode alphabet. its value gets reset to zero each time nearest is called, so don't expect it to stick around. ๐Ÿฆ‹

alphabet_rejections

my $rejected = $tf->alphabet_rejections ();

after running nearest over an array, this returns the number of entries of the array which were rejected using only the non-Unicode alphabet. same deal โ€” its value is reset to zero every time nearest is called. โœจ

length_rejections

my $rejected = $tf->length_rejections ();

after running nearest over an array, this gives you the number of entries of the array that got rejected because the length difference between them and the target string was larger than the maximum distance allowed. bye bye, too-long and too-short candidates. ๐Ÿ’€

get_max_distance

# Get the maximum edit distance.
print "The max distance is ", $tf->get_max_distance (), "\n";

gets the maximum edit distance of $tf. and if you wanna set it? that's what set_max_distance is for, bestie. ๐Ÿซถ


๐ŸŒธ SEE ALSO ๐ŸŒธ

Other CPAN modules

similar modules on CPAN include the following, go stan them:

  • String::Approx โ€” approximate matching (fuzzy matching) using the Levenshtein edit distance. as a bonus, if you don't already have a headache, you can get one easily trying to make head or tail out of this module's documentation. ๐Ÿ˜ญ
  • Text::EditTranscript โ€” determine the edit transcript between two strings. this is similar to what you get from distance_edits in this module.
  • Text::Fuzzy::PP โ€” this is Nick Logan's Pure Perl version of this module. ๐ŸŽ€
  • Text::Levenshtein::Damerau โ€” Levenshtein-Damerau edit distance.
  • Text::Levenshtein
  • Text::Levenshtein::Flexible โ€” XS Levenshtein distance calculation with bounds and adjustable costs (so the cost of deletion can be more than the cost of addition, etc.). see also Text::WagnerFischer for a pure-Perl module which also allows altered costs.
  • Text::Levenshtein::XS โ€” an XS implementation of the Levenshtein edit distance. it claims to be a drop-in replacement for Text::LevenshteinXS which does Unicode correctly. ๐Ÿ‘‘
  • Text::LevenshteinXS โ€” an XS implementation of the Levenshtein edit distance. does not do Unicode very well. see <https://rt.cpan.org/Public/Bug/Display.html?id=36685>. ๐Ÿ™ˆ
  • Text::Levenshtein::Edlib โ€” a wrapper around the edlib library that computes Levenshtein edit distance and optimal alignment path for a pair of strings.
  • Tree::BK โ€” structure for efficient fuzzy matching.
  • Text::Brew โ€” an implementation of the Brew edit distance.
  • Text::WagnerFischer โ€” implements the Wagner-Fischer algorithm to calculate edit distances. this is a generalised version of the Levenshtein edit distance. see also Text::Levenshtein::Flexible for an XS version.
  • Bencher::Scenario::LevenshteinModules โ€” some benchmarks of various modules including this one.
  • Text::JaroWinkler โ€” another text similarity measure.

About the algorithms

this section has some blog posts that the author found useful for understanding the algorithms. ๐Ÿ“š

honestly, the author recommends steering fairly clear of the Wikipedia articles on these things โ€” they're very poorly written indeed. ๐Ÿ’€ consider yourself warned bestie.

References

here are the original research papers by the algorithms' discoverers, for the academic girlies. ๐ŸŽ“

  • Damerau โ€” Damerau, Fred J. (March 1964), "A technique for computer detection and correction of spelling errors", Communications of the ACM, ACM, 7 (3): 171โ€“176, doi:10.1145/363958.363994
  • Levenshtein โ€” Levenshtein, Vladimir I. (February 1966), "Binary codes capable of correcting deletions, insertions, and reversals", Soviet Physics Doklady, 10 (8): 707โ€“710
  • Wagner and Fischer โ€” R. Wagner and M. Fischer (1974), "The string to string correction problem", Journal of the ACM, 21:168-178, doi:10.1145/321796.321811

๐Ÿฆ‹ HISTORY ๐Ÿฆ‹

0.26

  • a bug was fixed where an input string may be overwritten in code like
my $tf = Text::Fuzzy->new ($x);
$tf->distance ($y);

if $x is a plain ASCII string and $y is a Unicode string. ๐Ÿ˜ฑ (imagine getting your own string clobbered. not the vibe.)

  • links were added to the examples, and the outputs of the examples were added as part of the documentation. โœจ

0.28

  • the transposition code (the implementation of the Damerau-Levenshtein distance) was completely rewritten to make it more efficient. slay. the back-indexing of strings to find transpositions was changed so that the index of the object's string (the first argument to new) is preserved from one query to the next. a useless indexing of characters in the other string was removed. the structure used to hold the characters was changed from an unsorted allocated linked list to a sorted array in the case of Unicode strings, and a 256 character array in the case of non-Unicode strings. the oddly-named variables were renamed to more meaningful names. glow-up fr fr. ๐Ÿ’…
  • the transposition code now allows for a maximum distance to be set, beyond which no further matches will be allowed. ๐Ÿ”ฅ

๐Ÿซถ ACKNOWLEDGEMENTS ๐Ÿซถ

the edit distance including transpositions was contributed by Nick Logan (UGEXE). (this code was largely rewritten in version 0.28, so Nick Logan can no longer be held responsible for the Text::Fuzzy module's failings. ๐Ÿ˜ญ iconic of him to be honest.) some of the tests in t/trans.t are taken from the Text::Levenshtein::Damerau::XS module. Nils Boeffel reported a bug where strings may be overwritten in version 0.25. thank you legends. ๐ŸŒŸ

๐Ÿ‘‘ AUTHOR ๐Ÿ‘‘

Ben Bullock, <bkb@cpan.org>

this package and associated files are copyright (C) 2012-2020 Ben Bullock.

you can use, copy, modify and redistribute this package and associated files under the Perl Artistic Licence or the GNU General Public Licence. ๐Ÿซถ


and that's the whole page bestie!! this edition dropped as part of perl v5.32.1, dated 2022-01-21 โ€” go forth and match some fuzzy strings, you've got this. ๐Ÿ’–โœจ๐ŸŽ€

๐Ÿ“ฆ where this page comes from