├── .gitignore ├── .markdownlint.json ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── string_similarity_example.dart ├── lib ├── src │ ├── extensions │ │ └── string_extensions.dart │ ├── models │ │ ├── best_match.dart │ │ └── rating.dart │ └── string_similarity_base.dart └── string_similarity.dart ├── pubspec.yaml └── test ├── models ├── best_match_test.dart └── rating_test.dart └── string_similarity_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD024": false, 3 | "MD041": false, 4 | "MD013": false 5 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Dart", 9 | "program": "bin/main.dart", 10 | "request": "launch", 11 | "type": "dart" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 2 | - migrated to null safety 3 | - require dart sdk >= 2.12 4 | 5 | ## 1.1.0 6 | 7 | - added extensions. 8 | - added 'toString()' methods for BestMatch and Rating classes for better debugging. 9 | - require dart sdk >= 2.6 10 | 11 | ## 1.0.2 12 | 13 | - fix static analysis 14 | 15 | ## 1.0.1 16 | 17 | - update doc 18 | 19 | ## 1.0.0 20 | 21 | - initial version 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright 2019 Jérémy Landon 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # string-similarity 2 | 3 | Finds degree of similarity between two strings, based on [Dice's Coefficient](https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient), which is mostly better than [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance). 4 | 5 | ## :page_facing_up: Table of Contents 6 | 7 | - [Usage](#usage) 8 | - [API](#api) 9 | - ['string'.similarityTo(other)](#stringsimilarityToother) 10 | - [Arguments](#arguments) 11 | - [Returns](#returns) 12 | - [Examples](#examples) 13 | - ['string'.bestMatch(targetStrings)](#stringbestMatchtargetStrings) 14 | - [Arguments](#arguments-1) 15 | - [Returns](#returns-1) 16 | - [Examples](#examples-1) 17 | 18 | ## :video_game: Usage 19 | 20 | In your code: 21 | 22 | ```dart 23 | import 'package:string_similarity/string_similarity.dart'; 24 | 25 | var similarity = 'french'.similarityTo('quebec'); // or StringSimilarity.compareTwoStrings('french', 'quebec'); 26 | 27 | var matches = 'healed'.bestMatch(['edward', 'sealed', 'theatre']); // or StringSimilarity.findBestMatch('healed', ['edward', 'sealed', 'theatre']); 28 | ``` 29 | 30 | ## :books: API 31 | 32 | ### 'string'.similarityTo(other) 33 | 34 | Returns a fraction between 0 and 1, which indicates the degree of similarity between the two strings. 0 indicates completely different strings, 1 indicates identical strings. The comparison is case and diacritic sensitive. 35 | 36 | #### Arguments 37 | 38 | - other (String): The second string 39 | 40 | Order does not make a difference. 41 | 42 | #### Returns 43 | 44 | (double): A fraction from 0 to 1, both inclusive. Higher number indicates more similarity. 45 | 46 | #### Examples 47 | 48 | ```dart 49 | 'healed'.similarityTo('sealed'); // → 0.8 50 | 51 | 'france'.similarityTo('FrancE'); // → 0.6 52 | 53 | 'x'.similarityTo(null); // → 0.0 54 | 55 | 'Olive-green table for sale, in extremely good condition.'.similarityTo('For sale: table in very good condition, olive green in colour.'); // → 0.6060606060606061 56 | ``` 57 | 58 | or you can use the `StringSimilarity.compareTwoStrings` static method 59 | 60 | ```dart 61 | StringSimilarity.compareTwoStrings('healed', 'sealed'); // → 0.8 62 | ``` 63 | 64 | ### 'string'.bestMatch(targetStrings) 65 | 66 | Compares `mainString` against each string in `targetStrings`. 67 | 68 | #### Arguments 69 | 70 | - targetStrings (List\): Each string in this array will be matched against the main string. 71 | 72 | #### Returns 73 | 74 | (BestMatch): An object with a `ratings` property, which gives a similarity rating for each target string, a `bestMatch` property, which specifies which target string was most similar to the main string, and a `bestMatchIndex` property, which specifies the index of the bestMatch in the targetStrings array. 75 | 76 | #### Examples 77 | 78 | ```dart 79 | 'Olive-green table for sale, in extremely good condition.'.bestMatch([ 80 | 'For sale: green Subaru Impreza, 210,000 miles', 81 | 'For sale: table in very good condition, olive green in colour.', 82 | 'Wanted: mountain bike with at least 21 gears.', 83 | null 84 | ]); 85 | // → 86 | { ratings:[ 87 | { target: 'For sale: green Subaru Impreza, 210,000 miles', rating: 0.2558139534883721 }, 88 | { target: 'For sale: table in very good condition, olive green in colour.', rating: 0.6060606060606061 }, 89 | { target: 'Wanted: mountain bike with at least 21 gears.', rating: 0.1411764705882353 }, 90 | { target: null, rating: 0.0 } 91 | ], 92 | bestMatch: { target: 'For sale: table in very good condition, olive green in colour.', rating: 0.6060606060606061 }, 93 | bestMatchIndex: 1 94 | } 95 | ``` 96 | 97 | or you can use the `StringSimilarity.findBestMatch` static method 98 | 99 | ```dart 100 | StringSimilarity.findBestMatch('Olive-green table for sale, in extremely good condition.', [ 101 | 'For sale: green Subaru Impreza, 210,000 miles', 102 | 'For sale: table in very good condition, olive green in colour.', 103 | 'Wanted: mountain bike with at least 21 gears.', 104 | null 105 | ]); 106 | ``` 107 | 108 | ## :crystal_ball: Credit 109 | 110 | **_based on 'string-similarity' Javascript project_** : [https://github.com/aceakash/string-similarity](https://github.com/aceakash/string-similarity) 111 | 112 | thanks [@shinayser](https://github.com/shinayser) and [@nilsreichardt](https://github.com/nilsreichardt) -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Specify analysis options. 2 | # 3 | # Until there are meta linter rules, each desired lint must be explicitly enabled. 4 | # See: https://github.com/dart-lang/linter/issues/288 5 | # 6 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/ 7 | # See the configuration guide for more 8 | # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer 9 | # 10 | # There are other similar analysis options files in the flutter repos, 11 | # which should be kept in sync with this file: 12 | # 13 | # - analysis_options.yaml (this file) 14 | # - packages/flutter/lib/analysis_options_user.yaml 15 | # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml 16 | # - https://github.com/flutter/engine/blob/master/analysis_options.yaml 17 | # 18 | # This file contains the analysis options used by Flutter tools, such as IntelliJ, 19 | # Android Studio, and the `flutter analyze` command. 20 | 21 | analyzer: 22 | strong-mode: 23 | implicit-dynamic: false 24 | errors: 25 | # treat missing required parameters as a warning (not a hint) 26 | missing_required_param: warning 27 | # treat missing returns as a warning (not a hint) 28 | missing_return: warning 29 | # allow having TODOs in the code 30 | todo: ignore 31 | exclude: 32 | - 'bin/cache/**' 33 | # the following two are relative to the stocks example and the flutter package respectively 34 | # see https://github.com/dart-lang/sdk/issues/28463 35 | - 'lib/i18n/stock_messages_*.dart' 36 | - 'lib/src/http/**' 37 | - 'lib/models/*.g.dart' 38 | 39 | linter: 40 | rules: 41 | # these rules are documented on and in the same order as 42 | # the Dart Lint rules page to make maintenance easier 43 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 44 | - always_declare_return_types 45 | - always_put_control_body_on_new_line 46 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 47 | # - always_specify_types 48 | - annotate_overrides 49 | - avoid_bool_literals_in_conditional_expressions 50 | # - avoid_catches_without_on_clauses # we do this commonly 51 | # - avoid_catching_errors # we do this commonly 52 | # - avoid_double_and_int_checks # only useful when targeting JS runtime 53 | - avoid_empty_else 54 | - avoid_field_initializers_in_const_classes 55 | - avoid_function_literals_in_foreach_calls 56 | # - avoid_implementing_value_types # not yet tested 57 | - avoid_init_to_null 58 | # - avoid_js_rounded_ints # only useful when targeting JS runtime 59 | - avoid_null_checks_in_equality_operators 60 | # - avoid_positional_boolean_parameters # not yet tested 61 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 62 | - avoid_relative_lib_imports 63 | - avoid_renaming_method_parameters 64 | - avoid_return_types_on_setters 65 | # - avoid_returning_null # there are plenty of valid reasons to return null 66 | # - avoid_returning_null_for_future # not yet tested 67 | - avoid_returning_null_for_void 68 | # - avoid_returning_this # there are plenty of valid reasons to return this 69 | # - avoid_setters_without_getters # not yet tested 70 | # - avoid_shadowing_type_parameters # not yet tested 71 | # - avoid_single_cascade_in_expression_statements # not yet tested 72 | - avoid_slow_async_io 73 | - avoid_types_as_parameter_names 74 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 75 | - avoid_unused_constructor_parameters 76 | - avoid_void_async 77 | - await_only_futures 78 | - camel_case_types 79 | - cancel_subscriptions 80 | # - cascade_invocations # not yet tested 81 | # - close_sinks # not reliable enough 82 | # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765 83 | # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 84 | - control_flow_in_finally 85 | # - curly_braces_in_flow_control_structures # not yet tested 86 | # - diagnostic_describe_all_properties # not yet tested 87 | - directives_ordering 88 | - empty_catches 89 | - empty_constructor_bodies 90 | - empty_statements 91 | # - file_names # not yet tested 92 | - flutter_style_todos 93 | - hash_and_equals 94 | - implementation_imports 95 | # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 96 | # - join_return_with_assignment # not yet tested 97 | - library_names 98 | - library_prefixes 99 | # - lines_longer_than_80_chars # not yet tested 100 | # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 101 | - no_adjacent_strings_in_list 102 | - no_duplicate_case_values 103 | - non_constant_identifier_names 104 | # - null_closures # not yet tested 105 | - omit_local_variable_types # opposite of always_specify_types 106 | # - one_member_abstracts # too many false positives 107 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 108 | - overridden_fields 109 | - package_api_docs 110 | - package_names 111 | - package_prefixed_library_names 112 | # - parameter_assignments # we do this commonly 113 | - prefer_adjacent_string_concatenation 114 | - prefer_asserts_in_initializer_lists 115 | # - prefer_asserts_with_message # not yet tested 116 | - prefer_collection_literals 117 | - prefer_conditional_assignment 118 | - prefer_const_constructors 119 | - prefer_const_constructors_in_immutables 120 | - prefer_const_declarations 121 | - prefer_const_literals_to_create_immutables 122 | # - prefer_constructors_over_static_methods # not yet tested 123 | - prefer_contains 124 | # - prefer_double_quotes # opposite of prefer_single_quotes 125 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 126 | - prefer_final_fields 127 | # - prefer_final_in_for_each # not yet tested 128 | - prefer_final_locals 129 | # - prefer_for_elements_to_map_fromIterable # not yet tested 130 | - prefer_foreach 131 | # - prefer_function_declarations_over_variables # not yet tested 132 | - prefer_generic_function_type_aliases 133 | # - prefer_if_elements_to_conditional_expressions # not yet tested 134 | # - prefer_if_null_operators # not yet tested 135 | - prefer_initializing_formals 136 | - prefer_inlined_adds 137 | # - prefer_int_literals # not yet tested 138 | # - prefer_interpolation_to_compose_strings # not yet tested 139 | - prefer_is_empty 140 | - prefer_is_not_empty 141 | - prefer_iterable_whereType 142 | # - prefer_mixin # https://github.com/dart-lang/language/issues/32 143 | # - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932 144 | - prefer_single_quotes 145 | - prefer_spread_collections 146 | - prefer_typing_uninitialized_variables 147 | - prefer_void_to_null 148 | # - provide_deprecation_message # not yet tested 149 | # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml 150 | - recursive_getters 151 | - slash_for_doc_comments 152 | # - sort_child_properties_last # not yet tested 153 | - sort_constructors_first 154 | - sort_pub_dependencies 155 | - sort_unnamed_constructors_first 156 | - test_types_in_equals 157 | - throw_in_finally 158 | # - type_annotate_public_apis # subset of always_specify_types 159 | - type_init_formals 160 | # - unawaited_futures # too many false positives 161 | # - unnecessary_await_in_return # not yet tested 162 | - unnecessary_brace_in_string_interps 163 | - unnecessary_const 164 | - unnecessary_getters_setters 165 | # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 166 | - unnecessary_new 167 | - unnecessary_null_aware_assignments 168 | - unnecessary_null_in_if_null_operators 169 | - unnecessary_overrides 170 | - unnecessary_parenthesis 171 | - unnecessary_statements 172 | - unnecessary_this 173 | - unrelated_type_equality_checks 174 | # - unsafe_html # not yet tested 175 | - use_full_hex_values_for_flutter_colors 176 | # - use_function_type_syntax_for_parameters # not yet tested 177 | - use_rethrow_when_possible 178 | # - use_setters_to_change_properties # not yet tested 179 | # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 180 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 181 | - valid_regexps 182 | # - void_checks # not yet tested 183 | -------------------------------------------------------------------------------- /example/string_similarity_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:string_similarity/string_similarity.dart'; 2 | 3 | void main() { 4 | // compare two strings 5 | final comparison = 'healed'.similarityTo( 6 | 'sealed'); // or StringSimilarity.compareTwoStrings('healed', 'sealed') 7 | print(comparison); // → 0.8 8 | 9 | print(null.similarityTo(null)); // -> 1.0 10 | print('france'.similarityTo(null)); // -> 0.0 11 | 12 | // get the best match 13 | const mainString = 'Olive-green table for sale, in extremely good condition.'; 14 | const targetStrings = [ 15 | 'For sale: green Subaru Impreza, 210,000 miles', 16 | 'For sale: table in very good condition, olive green in colour.', 17 | 'Wanted: mountain bike with at least 21 gears.', 18 | null 19 | ]; 20 | final bestMatch = mainString.bestMatch( 21 | targetStrings); // or StringSimilarity.findBestMatch(mainString, targetStrings) 22 | print( 23 | bestMatch); // → 1:'For sale: table in very good condition, olive green in colour.'[0.6060606060606061] 24 | print(bestMatch 25 | .ratings); // → ['For sale: green Subaru Impreza, 210,000 miles'[0.2558139534883721], 'For sale: table in very good condition, olive green in colour.'[0.6060606060606061], 'Wanted: mountain bike with at least 21 gears.'[0.1411764705882353]] 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/extensions/string_extensions.dart: -------------------------------------------------------------------------------- 1 | import '../../string_similarity.dart'; 2 | 3 | extension StringExtensions on String? { 4 | /// Returns a fraction between 0 and 1, which indicates the degree of similarity between the two strings. 0 indicates completely different strings, 1 indicates identical strings. The comparison is case-sensitive. 5 | /// 6 | /// _(same as StringSimilarity.compareTwoStrings method)_ 7 | /// 8 | /// ##### Arguments 9 | /// - other (String?): The second string 10 | /// 11 | /// (Order does not make a difference) 12 | /// 13 | /// ##### Returns 14 | /// (number): A fraction from 0 to 1, both inclusive. Higher number indicates more similarity. 15 | double similarityTo(String? other) => 16 | StringSimilarity.compareTwoStrings(this, other); 17 | 18 | /// Compares mainString against each string in targetStrings. 19 | /// 20 | /// _(same as StringSimilarity.findBestMatch method)_ 21 | /// 22 | /// ##### Arguments 23 | /// - targetStrings (List): Each string in this array will be matched against the main string. 24 | /// 25 | /// ##### Returns 26 | /// (BestMatch): An object with a ratings property, which gives a similarity rating for each target string, a bestMatch property, which specifies which target string was most similar to the main string, and a bestMatchIndex property, which specifies the index of the bestMatch in the targetStrings array. 27 | BestMatch bestMatch(List targetStrings) => 28 | StringSimilarity.findBestMatch(this, targetStrings); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/models/best_match.dart: -------------------------------------------------------------------------------- 1 | import 'rating.dart'; 2 | 3 | /// Dice's Coefficient results 4 | class BestMatch { 5 | BestMatch( 6 | {required this.ratings, 7 | required this.bestMatch, 8 | required this.bestMatchIndex}); 9 | 10 | /// similarity rating for each target string 11 | List ratings; 12 | 13 | /// specifies which target string was most similar to the main string 14 | Rating bestMatch; 15 | 16 | /// which specifies the index of the bestMatch in the targetStrings array 17 | int bestMatchIndex; 18 | 19 | @override 20 | String toString() => '$bestMatchIndex:${bestMatch.toString()}'; 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/models/rating.dart: -------------------------------------------------------------------------------- 1 | /// Dice's Coefficient result 2 | class Rating { 3 | Rating({this.target, this.rating}); 4 | 5 | /// reference text 6 | String? target; 7 | 8 | /// between 0 and 1. 0 indicates completely different strings, 1 indicates identical strings. 9 | double? rating; 10 | 11 | @override 12 | String toString() => '\'$target\'[$rating]'; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/string_similarity_base.dart: -------------------------------------------------------------------------------- 1 | import 'models/best_match.dart'; 2 | import 'models/rating.dart'; 3 | 4 | /// Finds degree of similarity between two strings, based on Dice's Coefficient, which is mostly better than Levenshtein distance. 5 | class StringSimilarity { 6 | /// Returns a fraction between 0 and 1, which indicates the degree of similarity between the two strings. 0 indicates completely different strings, 1 indicates identical strings. The comparison is case-sensitive. 7 | /// 8 | /// _(same as 'string'.similarityTo extension method)_ 9 | /// 10 | /// ##### Arguments 11 | /// - first (String?): The first string 12 | /// - second (String?): The second string 13 | /// 14 | /// (Order does not make a difference) 15 | /// 16 | /// ##### Returns 17 | /// (number): A fraction from 0 to 1, both inclusive. Higher number indicates more similarity. 18 | static double compareTwoStrings(String? first, String? second) { 19 | // if both are null 20 | if (first == null && second == null) { 21 | return 1; 22 | } 23 | // as both are not null if one of them is null then return 0 24 | if (first == null || second == null) { 25 | return 0; 26 | } 27 | 28 | first = 29 | first.replaceAll(RegExp(r'\s+\b|\b\s'), ''); // remove all whitespace 30 | second = 31 | second.replaceAll(RegExp(r'\s+\b|\b\s'), ''); // remove all whitespace 32 | 33 | // if both are empty strings 34 | if (first.isEmpty && second.isEmpty) { 35 | return 1; 36 | } 37 | // if only one is empty string 38 | if (first.isEmpty || second.isEmpty) { 39 | return 0; 40 | } 41 | // identical 42 | if (first == second) { 43 | return 1; 44 | } 45 | // both are 1-letter strings 46 | if (first.length == 1 && second.length == 1) { 47 | return 0; 48 | } 49 | // if either is a 1-letter string 50 | if (first.length < 2 || second.length < 2) { 51 | return 0; 52 | } 53 | 54 | final firstBigrams = {}; 55 | for (var i = 0; i < first.length - 1; i++) { 56 | final bigram = first.substring(i, i + 2); 57 | final count = 58 | firstBigrams.containsKey(bigram) ? firstBigrams[bigram]! + 1 : 1; 59 | firstBigrams[bigram] = count; 60 | } 61 | 62 | var intersectionSize = 0; 63 | for (var i = 0; i < second.length - 1; i++) { 64 | final bigram = second.substring(i, i + 2); 65 | final count = 66 | firstBigrams.containsKey(bigram) ? firstBigrams[bigram]! : 0; 67 | 68 | if (count > 0) { 69 | firstBigrams[bigram] = count - 1; 70 | intersectionSize++; 71 | } 72 | } 73 | 74 | return (2.0 * intersectionSize) / (first.length + second.length - 2); 75 | } 76 | 77 | /// Compares mainString against each string in targetStrings 78 | /// 79 | /// _(same as 'string'.bestMatch extension method)_ 80 | /// 81 | /// ##### Arguments 82 | /// - mainString (String?): The string to match each target string against. 83 | /// - targetStrings (List): Each string in this array will be matched against the main string. 84 | /// 85 | /// ##### Returns 86 | /// (BestMatch): An object with a ratings property, which gives a similarity rating for each target string, a bestMatch property, which specifies which target string was most similar to the main string, and a bestMatchIndex property, which specifies the index of the bestMatch in the targetStrings array. 87 | static BestMatch findBestMatch( 88 | String? mainString, List targetStrings) { 89 | final ratings = []; 90 | var bestMatchIndex = 0; 91 | 92 | for (var i = 0; i < targetStrings.length; i++) { 93 | final currentTargetString = targetStrings[i]; 94 | final currentRating = compareTwoStrings(mainString, currentTargetString); 95 | ratings.add(Rating(target: currentTargetString, rating: currentRating)); 96 | if (currentRating > ratings[bestMatchIndex].rating!) { 97 | bestMatchIndex = i; 98 | } 99 | } 100 | 101 | final bestMatch = ratings[bestMatchIndex]; 102 | 103 | return BestMatch( 104 | ratings: ratings, bestMatch: bestMatch, bestMatchIndex: bestMatchIndex); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/string_similarity.dart: -------------------------------------------------------------------------------- 1 | library string_similarity; 2 | 3 | export 'src/extensions/string_extensions.dart'; 4 | export 'src/models/best_match.dart'; 5 | export 'src/models/rating.dart'; 6 | export 'src/string_similarity_base.dart'; 7 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: string_similarity 2 | description: Finds degree of similarity between two strings, based on Dice's Coefficient, which is mostly better than Levenshtein distance. 3 | version: 2.1.1 4 | maintainers: Jérémy LANDON 5 | homepage: https://github.com/jeremylandon/string-similarity 6 | 7 | environment: 8 | sdk: '>=2.12.0 <4.0.0' 9 | 10 | dev_dependencies: 11 | pedantic: ^1.11.0 12 | test: ^1.16.7 13 | -------------------------------------------------------------------------------- /test/models/best_match_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:string_similarity/string_similarity.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('BestMatch', () { 6 | test('toString return "index:rating"', () { 7 | final bestRating = Rating(rating: 1.23456, target: 'str'); 8 | final bestMatch = BestMatch( 9 | bestMatchIndex: 1, 10 | bestMatch: bestRating, 11 | ratings: [Rating(rating: 0.01, target: 'str'), bestRating]); 12 | 13 | expect(bestMatch.toString(), '1:\'str\'[1.23456]'); 14 | }); 15 | 16 | test('toString with null target return "index:rating"', () { 17 | final bestRating = Rating(rating: 1.23456, target: null); 18 | final bestMatch = BestMatch( 19 | bestMatchIndex: 1, 20 | bestMatch: bestRating, 21 | ratings: [Rating(rating: 0.01, target: 'str'), bestRating]); 22 | 23 | expect(bestMatch.toString(), '1:\'null\'[1.23456]'); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/models/rating_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:string_similarity/string_similarity.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Rating', () { 6 | test('toString return "target:rating{complete}"', () { 7 | final rating = Rating(rating: 1 / 3, target: 'str'); 8 | 9 | expect(rating.toString(), '\'str\'[0.3333333333333333]'); 10 | }); 11 | 12 | test('toString return "target:rating" without useless numbers', () { 13 | final rating = Rating(rating: 0.10000000000, target: 'str'); 14 | 15 | expect(rating.toString(), '\'str\'[0.1]'); 16 | }); 17 | 18 | test('toString with null target return "null:rating"', () { 19 | final rating = Rating(rating: 0.1, target: null); 20 | 21 | expect(rating.toString(), '\'null\'[0.1]'); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/string_similarity_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:string_similarity/string_similarity.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | class TestData { 5 | TestData({this.sentenceA, this.sentenceB, required this.expected}); 6 | 7 | String? sentenceA; 8 | String? sentenceB; 9 | double expected; 10 | } 11 | 12 | void main() { 13 | late List _testData; 14 | group('compareTwoStrings', () { 15 | setUp(() { 16 | _testData = [ 17 | TestData(sentenceA: 'french', sentenceB: 'quebec', expected: 0), 18 | TestData(sentenceA: 'france', sentenceB: 'france', expected: 1), 19 | TestData(sentenceA: 'fRaNce', sentenceB: 'france', expected: 0.2), 20 | TestData( 21 | sentenceA: 'corée du sud', 22 | sentenceB: 'coree du sud', 23 | expected: 0.7777777777777778), 24 | TestData(sentenceA: 'healed', sentenceB: 'sealed', expected: 0.8), 25 | TestData( 26 | sentenceA: 'web applications', 27 | sentenceB: 'applications of the web', 28 | expected: 0.7878787878787878), 29 | TestData( 30 | sentenceA: 'this will have a typo somewhere', 31 | sentenceB: 'this will huve a typo somewhere', 32 | expected: 0.92), 33 | TestData( 34 | sentenceA: 'Olive-green table for sale, in extremely good condition.', 35 | sentenceB: 36 | 'For sale: table in very good condition, olive green in colour.', 37 | expected: 0.6060606060606061, 38 | ), 39 | TestData( 40 | sentenceA: 'Olive-green table for sale, in extremely good condition.', 41 | sentenceB: 'For sale: green Subaru Impreza, 210,000 miles', 42 | expected: 0.2558139534883721, 43 | ), 44 | TestData( 45 | sentenceA: 'Olive-green table for sale, in extremely good condition.', 46 | sentenceB: 'Wanted: mountain bike with at least 21 gears.', 47 | expected: 0.1411764705882353, 48 | ), 49 | TestData( 50 | sentenceA: 'this has one extra word', 51 | sentenceB: 'this has one word', 52 | expected: 0.7741935483870968), 53 | TestData(sentenceA: 'A', sentenceB: 'a', expected: 0), 54 | TestData(sentenceA: 'a', sentenceB: 'a', expected: 1), 55 | TestData(sentenceA: 'a', sentenceB: 'b', expected: 0), 56 | TestData(sentenceA: '', sentenceB: '', expected: 1), 57 | TestData(sentenceA: 'a', sentenceB: '', expected: 0), 58 | TestData(sentenceA: '', sentenceB: 'a', expected: 0), 59 | TestData( 60 | sentenceA: 'apple event', sentenceB: 'apple event', expected: 1), 61 | TestData( 62 | sentenceA: 'iphone', 63 | sentenceB: 'iphone x', 64 | expected: 0.9090909090909091), 65 | TestData( 66 | sentenceA: '10아이', sentenceB: '10아기', expected: 0.6666666666666666), 67 | TestData(sentenceA: null, sentenceB: 'b', expected: 0), 68 | TestData(sentenceA: 'a', sentenceB: null, expected: 0), 69 | TestData(sentenceA: null, sentenceB: null, expected: 1), 70 | ]; 71 | }); 72 | 73 | test('returns the correct value for different inputs', () { 74 | for (var td in _testData) { 75 | expect(StringSimilarity.compareTwoStrings(td.sentenceA, td.sentenceB), 76 | td.expected); 77 | } 78 | }); 79 | 80 | test( 81 | 'similarityTo extensions method return same result that StringSimilarity.compareTwoStrings', 82 | () { 83 | for (var td in _testData) { 84 | final a = 85 | StringSimilarity.compareTwoStrings(td.sentenceA, td.sentenceB); 86 | final b = td.sentenceA.similarityTo(td.sentenceB); 87 | 88 | expect(a.toString(), b.toString()); 89 | } 90 | }); 91 | }); 92 | 93 | group('findBestMatch', () { 94 | setUp(() { 95 | _testData = [ 96 | TestData(sentenceA: 'mailed', expected: 0.4), 97 | TestData(sentenceA: 'edward', expected: 0.2), 98 | TestData(sentenceA: 'sealed', expected: 0.8), 99 | TestData(sentenceA: 'theatre', expected: 0.36363636363636365), 100 | ]; 101 | }); 102 | 103 | test('assigns a similarity rating to each string passed in the array', () { 104 | final matches = StringSimilarity.findBestMatch('healed', 105 | _testData.map((TestData testEntry) => testEntry.sentenceA).toList()); 106 | 107 | for (var i = 0; i < matches.ratings.length; i++) { 108 | expect(_testData[i].sentenceA, matches.ratings[i].target); 109 | expect(_testData[i].expected, matches.ratings[i].rating); 110 | } 111 | }); 112 | 113 | test('returns the best match and its similarity rating', () { 114 | final matches = StringSimilarity.findBestMatch('healed', 115 | _testData.map((TestData testEntry) => testEntry.sentenceA).toList()); 116 | 117 | expect(matches.bestMatch.target, 'sealed'); 118 | expect(matches.bestMatch.rating, 0.8); 119 | }); 120 | 121 | test('returns the index of best match from the target strings', () { 122 | final matches = StringSimilarity.findBestMatch('healed', 123 | _testData.map((TestData testEntry) => testEntry.sentenceA).toList()); 124 | 125 | expect(matches.bestMatchIndex, 2); 126 | }); 127 | 128 | test( 129 | 'bestMatch extensions method return same result that StringSimilarity.findBestMatch', 130 | () { 131 | const mainString = 'healed'; 132 | final targetStrings = 133 | _testData.map((TestData testEntry) => testEntry.sentenceA).toList(); 134 | final a = StringSimilarity.findBestMatch(mainString, targetStrings); 135 | final b = mainString.bestMatch(targetStrings); 136 | 137 | expect(a.toString(), b.toString()); 138 | }); 139 | 140 | test('bestMatch extensions method can be call on null anonymous variable', 141 | () { 142 | final a = null.bestMatch([null]); 143 | final b = null.bestMatch(['a']); 144 | 145 | expect(a.bestMatch.rating, 1); 146 | expect(b.bestMatch.rating, 0); 147 | }); 148 | }); 149 | } 150 | --------------------------------------------------------------------------------