diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b826fbb --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Liu Xin + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 94946d4..497903f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # pylcs + +**The [original repository](https://github.com/Meteorix/pylcs) stop maintenance. This is a transfer version** + **pylcs** is a super fast c++ library which adopts dynamic programming(DP) algorithm to solve two classic LCS problems as below . [The longest common subsequence](https://en.wikipedia.org/wiki/Longest_common_subsequence_problem) problem is the problem of finding the longest subsequence common to all sequences in a set of sequences (often just two sequences). @@ -9,6 +12,8 @@ We also support Chinese(or any UTF-8) string. +Colorful Visualization: After 0.1.0, you can visualize the lcs result with colorful output. + Install ------- @@ -25,36 +30,112 @@ import pylcs # finding the longest common subsequence length of string A and string B A = 'We are shannonai' B = 'We like shannonai' -pylcs.lcs(A, B) +pylcs.lcs_sequence_length(A, B) """ ->>> pylcs.lcs(A, B) +>>> pylcs.lcs_sequence_length(A, B) 14 """ +# finding alignment from string A to B +A = 'We are shannonai' +B = 'We like shannonai' +res = pylcs.lcs_sequence_idx(A, B) +''.join([B[i] for i in res if i != -1]) +""" +>>> res +[0, 1, 2, -1, -1, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] +>>> ''.join([B[i] for i in res if i != -1]) +'We e shannonai' +""" + # finding the longest common subsequence length of string A and a list of string B A = 'We are shannonai' B = ['We like shannonai', 'We work in shannonai', 'We are not shannonai'] -pylcs.lcs_of_list(A, B) +pylcs.lcs_sequence_of_list(A, B) """ ->>> pylcs.lcs_of_list(A, B) +>>> pylcs.lcs_sequence_of_list(A, B) [14, 14, 16] """ # finding the longest common substring length of string A and string B A = 'We are shannonai' B = 'We like shannonai' -pylcs.lcs2(A, B) +pylcs.lcs_string_length(A, B) """ ->>> pylcs.lcs2(A, B) +>>> pylcs.lcs_string_length(A, B) 11 """ +# finding alignment from string A to B +A = 'We are shannonai' +B = 'We like shannonai' +res = pylcs.lcs_string_idx(A, B) +''.join([B[i] for i in res if i != -1]) +""" +>>> res +[-1, -1, -1, -1, -1, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] +>>> ''.join([B[i] for i in res if i != -1]) +'e shannonai' +""" + # finding the longest common substring length of string A and a list of string B A = 'We are shannonai' B = ['We like shannonai', 'We work in shannonai', 'We are not shannonai'] -pylcs.lcs2_of_list(A, B) +pylcs.lcs_string_of_list(A, B) """ ->>> pylcs.lcs2_of_list(A, B) +>>> pylcs.lcs_string_of_list(A, B) [11, 10, 10] """ +# finding the weighted edit distance from string A to B +pylcs.edit_distance("aaa", "aba") +pylcs.edit_distance("aaa", "aba", {'a': {'b': 2.0}}) +pylcs.edit_distance("", "aa", {'': {'a': 0.5}}) +# weight['']['a'] means inserting a char 'a' costs 0.5 +# similarly, weight['a'][''] means the score of deleting a char 'a' +""" +>>> pylcs.edit_distance("aaa", "aba") +1 +>>> pylcs.edit_distance("aaa", "aba", {'a': {'b': 2.0}}) +2.0 +>>> pylcs.edit_distance("", "aa", {'': {'a': 0.5}}) +1.0 +""" + +# finding edit distance alignment from string A to B +pylcs.edit_distance_idx("aaa", "aba") +pylcs.edit_distance_idx("aaa", "aba", {'a': {'b': 3}}) +pylcs.edit_distance_idx("aa", "aabb", {'a': {'a': 2, 'b': 0}}) +""" +>>> pylcs.edit_distance_idx("aaa", "aba") +[0, 1, 2] +>>> pylcs.edit_distance_idx("aaa", "aba", {'a': {'b': 3}}) +[0, -1, 2] +>>> pylcs.edit_distance_idx("aa", "aabb", {'a': {'a': 2, 'b': 0}}) +[2, 3] +""" +``` + +After 0.1.0, you can make a visualized comparison with colorful output. Using `coloring_match_sequence` to color the s1 and s2 by a match list like: +```python +s1, s2 = "abcdefghijklmnopq", "-c-fgh-kl-nop-q" +match_list = pylcs.lcs_sequence_idx(s1, s2) +colored_s1, colored_s2 = pylcs.coloring_match_sequence(match_list, s1, s2, 11, 11, "#2266ff", "#2266ff", t=1) +print(colored_s1, colored_s2) +colored_s1, colored_s2 = pylcs.coloring_match_sequence(match_list, s1, s2, 11, 11, "#2266ff", "#2266ff", t=2) +print(colored_s1, colored_s2) +colored_s1, colored_s2 = pylcs.coloring_match_sequence(match_list, s1, s2, 11, 11, "#2266ff", "#2266ff", t=3) +print(colored_s1, colored_s2) +``` +![](https://github.com/kuangkzh/pylcs/blob/master/tests/coloring_match_sequence.jpg) +```python +s1, s2 = "How does this string edit to s2?", "How similar is this string to s1?" +match_list = pylcs.edit_distance_idx(s1, s2) +colored_s1, colored_s2 = pylcs.coloring_match_sequence(match_list, s1, s2, 4, 4, 230, 230, t=2) +print(colored_s1, colored_s2, sep='\n') +``` +![](https://github.com/kuangkzh/pylcs/blob/master/tests/coloring_match_sequence_2.jpg) + +Note that the colorful output uses ANSI escape codes. Referring to https://en.wikipedia.org/wiki/ANSI_escape_code. + +The ANSI codes may not work in win32 command line. diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..1a1fb45 --- /dev/null +++ b/build.sh @@ -0,0 +1,32 @@ +#conda create -y -n py35 python=3.5 pybind11 +#conda create -y -n py36 python=3.6 pybind11 +#conda create -y -n py37 python=3.7 pybind11 +#conda create -y -n py38 python=3.8 pybind11 +#conda create -y -n py39 python=3.9 pybind11 +#conda create -y -n py310 python=3.10 pybind11 +#conda create -y -n py311 python=3.11 pybind11 + +conda activate py35 +python -m pip install --upgrade build +python -m build -w +conda activate py36 +python -m pip install --upgrade build +python -m build -w +conda activate py37 +python -m pip install --upgrade build +python -m build -w +conda activate py38 +python -m pip install --upgrade build +python -m build -w +conda activate py39 +python -m pip install --upgrade build +python -m build -w +conda activate py310 +python -m pip install --upgrade build +python -m build -w +conda activate py311 +python -m pip install --upgrade build +python -m build -w +python -m build -s + +py -m twine upload --repository pypi dist/* diff --git a/pylcs/__init__.py b/pylcs/__init__.py new file mode 100644 index 0000000..ecda6b1 --- /dev/null +++ b/pylcs/__init__.py @@ -0,0 +1,61 @@ +import re +from _pylcs import * + + +def color_to_ansi_escape_code(color, is_front=True): + if color is None: + return "" + color_mode = 38 if is_front else 48 + if isinstance(color, int): + return f"\033[{color_mode};5;{color}m" + if isinstance(color, str): + color = color.strip("#") + return f"\033[{color_mode};2;{int(color[0:2], 16)};{int(color[2:4], 16)};{int(color[4:6], 16)}m" + return color + + +def coloring_match_sequence(match_list, s1, s2, c1=None, c2=None, b1=None, b2=None, t=5): + """ + Coloring the s1 and s2 by match results in match_list. Returned the colored s1 and s2 using ANSI escape codes. + Referring to https://en.wikipedia.org/wiki/ANSI_escape_code. + The ANSI codes may not work in win32. In this time you can try colorama.init first(maybe useless): + >>> import colorama + >>> colorama.init(autoreset=True) + :param match_list: An array of match results with the same length as s1. Each element indicates the match index in + s2. Not match for -1. + :param s1: string1 + :param s2: string2 + :param c1: Option[int, str], Default None. Forefront color for s1. + Int color index in https://en.wikipedia.org/wiki/ANSI_escape_code#Colors. + Hex color string like "#FFFFFF" is also supported. + :param c2: Option[int, str], Default None. Forefront color for s2. + :param b1: Option[int, str], Default None. Background color for s1. + :param b2: Option[int, str], Default None. Background color for s2. + :param t: A continuous length threshold for coloring. + :return: (colored_s1, colored_s2) + """ + def _coloring(s, color, back_color): + color = color_to_ansi_escape_code(color, is_front=True) + back_color = color_to_ansi_escape_code(back_color, is_front=False) + + def _color_repl(re_obj): + _slice = s[re_obj.start(): re_obj.end()] + if re.match("1+", re_obj.group(0)): + return color + back_color + _slice + "\033[0m" + return _slice + + return _color_repl + + mask = ''.join(['1' if i != -1 else '0' for i in match_list]) + mask = re.sub("1+", lambda s: s.group(0).replace("1", "0" if len(s.group(0)) < t else "1"), mask) + + cs1 = re.sub("0+|1+", _coloring(s1, c1, b1), mask) + + mask = set(match_list) + mask = ''.join(['1' if i in mask else '0' for i in range(len(s2))]) + mask = re.sub("1+", lambda s: s.group(0).replace("1", "0" if len(s.group(0)) < t else "1"), mask) + + cs2 = re.sub("0+|1+", _coloring(s2, c2, b2), mask) + + return cs1, cs2 + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..83a7f7a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=42", "pybind11>=2.2"] +build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index bc70efe..56f2b89 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ def __str__(self): ext_modules = [ Extension( - 'pylcs', + '_pylcs', ['src/main.cpp'], include_dirs=[ # Path to pybind11 headers @@ -90,15 +90,17 @@ def build_extensions(self): setup( name='pylcs', - version='0.0.6', - author='Meteorix', - author_email='lxhustauto@gmail.com', - url='https://github.com/Meteorix/pylcs', + version='0.1.1', + author='kuangzh', + author_email='kuangzh@smail.nju.edu.cn', + url='https://github.com/kuangkzh/pylcs', description='super fast cpp implementation of longest common subsequence', long_description=long_description, long_description_content_type="text/markdown", + packages=["pylcs"], ext_modules=ext_modules, install_requires=['pybind11>=2.2'], cmdclass={'build_ext': BuildExt}, zip_safe=False, + license='Apache 2.0' ) diff --git a/src/main.cpp b/src/main.cpp index a0f342e..b15b18e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include using namespace std; @@ -28,7 +29,7 @@ vector utf8_split(const string &str){ // 最长公共子序列(不连续) -int lcs_length_(const string &str1, const string &str2) { +int lcs_sequence_length(const string &str1, const string &str2) { if (str1 == "" || str2 == "") return 0; vector s1 = utf8_split(str1); @@ -61,8 +62,51 @@ int lcs_length_(const string &str1, const string &str2) { } +vector lcs_sequence_idx(const string &str, const string &ref) { + vector s1 = utf8_split(str); + vector s2 = utf8_split(ref); + int m = s1.size(); + int n = s2.size(); + vector> dp(m + 1, vector(n + 1)); + vector> direct(m + 1, vector(n + 1)); + vector res(m, -1); + if (m == 0 || n == 0) + return res; + + int i, j; + for (i = 0; i <= m; i++) dp[i][0] = 0; + for (j = 0; j <= n; j++) dp[0][j] = 0; + for (i = 1; i <= m; i++) { + for (j = 1; j <= n; j++) { + if (s1[i - 1] == s2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + direct[i][j] = 'm'; // match + } else { + if (dp[i - 1][j] >= dp[i][j - 1]) { + dp[i][j] = dp[i - 1][j]; + direct[i][j] = 's'; // str+1 + } + else { + dp[i][j] = dp[i][j-1]; + direct[i][j] = 'r'; // ref+1 + } + } + } + } + for (i = m, j = n; i > 0 && j > 0; ){ + if (direct[i][j] == 'm') { + res[i-1] = j-1; + i--; j--; + } + else if (direct[i][j] == 's') i--; + else if (direct[i][j] == 'r') j--; + } + return res; +} + + // 最长公共子串(连续) -int lcs2_length_(const string &str1, const string &str2) { +int lcs_string_length(const string &str1, const string &str2) { if (str1 == "" || str2 == "") return 0; vector s1 = utf8_split(str1); @@ -96,34 +140,60 @@ int lcs2_length_(const string &str1, const string &str2) { } -// TODO 返回子序列 -int lcs(const string &str1, const string &str2){ - return lcs_length_(str1, str2); -} - +vector lcs_string_idx(const string &str, const string &ref) { + vector s1 = utf8_split(str); + vector s2 = utf8_split(ref); + int m = s1.size(); + int n = s2.size(); + vector> dp(m + 1, vector(n + 1)); + vector res(m, -1); + if (m == 0 || n == 0) + return res; -// TODO 返回子串 -int lcs2(const string &str1, const string &str2){ - return lcs2_length_(str1, str2); + int i, j; + int max_i = 0, max_j = 0; + for (i = 0; i <= m; i++) { + dp[i][0] = 0; + } + for (j = 0; j <= n; j++) { + dp[0][j] = 0; + } + for (i = 1; i <= m; i++) { + for (j = 1; j <= n; j++) { + if (s1[i - 1] == s2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + if (dp[i][j] > dp[max_i][max_j]){ + max_i = i; max_j = j; + } + } + else { + dp[i][j] = 0; + } + } + } + for (i = 0; i < dp[max_i][max_j]; i++) { + res[max_i-i-1] = max_j-i-1; + } + return res; } -vector lcs_of_list(const string &str1, vector &str_list){ +vector lcs_sequence_of_list(const string &str1, vector &str_list){ int size = str_list.size(); vector ls(size); for (int i = 0; i < size; i++){ - int l = lcs(str1, str_list[i]); + int l = lcs_sequence_length(str1, str_list[i]); ls[i] = l; } return ls; } -vector lcs2_of_list(const string &str1, vector &str_list){ +vector lcs_string_of_list(const string &str1, vector &str_list){ int size = str_list.size(); vector ls(size); for (int i = 0; i < size; i++){ - int l = lcs2(str1, str_list[i]); + int l = lcs_string_length(str1, str_list[i]); ls[i] = l; } return ls; @@ -132,8 +202,6 @@ vector lcs2_of_list(const string &str1, vector &str_list){ // 编辑距离 int levenshtein_distance(const string &str1, const string &str2) { - if (str1 == "" || str2 == "") - return 0; vector s1 = utf8_split(str1); vector s2 = utf8_split(str2); int m = s1.size(); @@ -160,6 +228,92 @@ int levenshtein_distance(const string &str1, const string &str2) { } +bool inline __contains(map> m, string k1, string k2) { + return m.find(k1) != m.end() && m[k1].find(k2) != m[k1].end(); +} + +float levenshtein_distance_weighted(const string &str1, const string &str2, map> &weight) { + vector s1 = utf8_split(str1); + vector s2 = utf8_split(str2); + string __(""); + int m = s1.size(); + int n = s2.size(); + vector> dp(m + 1, vector(n + 1)); + int i, j; + float si, sj, sij; + + dp[0][0] = 0; + for (i = 1; i <= m; i++) { + dp[i][0] = dp[i-1][0] + (__contains(weight, s1[i-1], __) ? weight[s1[i-1]][__] : 1); + } + for (j = 1; j <= n; j++) { + dp[0][j] = dp[0][j-1] + (__contains(weight, __, s2[j-1]) ? weight[__][s2[j-1]] : 1); + } + for (i = 1; i <= m; i++) { + for (j = 1; j <= n; j++) { + si = __contains(weight, s1[i-1], __) ? weight[s1[i-1]][__] : 1; + sj = __contains(weight, __, s2[j-1]) ? weight[__][s2[j-1]] : 1; + sij = __contains(weight, s1[i-1], s2[j-1]) ? weight[s1[i-1]][s2[j-1]] : (s1[i-1] == s2[j-1] ? 0 : 1); + dp[i][j] = min({dp[i-1][j] + si, dp[i][j-1] + sj, dp[i-1][j-1] + sij}); + } + } + return dp[m][n]; +} + + +vector levenshtein_distance_idx_weighted(const string &str, const string &ref, map> &weight) { + vector s1 = utf8_split(str); + vector s2 = utf8_split(ref); + string __(""); + int m = s1.size(); + int n = s2.size(); + vector> dp(m + 1, vector(n + 1)); + vector> direct(m + 1, vector(n + 1)); + vector res(m, -1); + int i, j; + float si, sj, sij; + + dp[0][0] = 0; + for (i = 1; i <= m; i++) { + dp[i][0] = dp[i-1][0] + (__contains(weight, s1[i-1], __) ? weight[s1[i-1]][__] : 1); + direct[i][0] = 's'; + } + for (j = 1; j <= n; j++) { + dp[0][j] = dp[0][j-1] + (__contains(weight, __, s2[j-1]) ? weight[__][s2[j-1]] : 1); + direct[0][j] = 'r'; + } + for (i = 1; i <= m; i++) { + for (j = 1; j <= n; j++) { + si = __contains(weight, s1[i-1], __) ? weight[s1[i-1]][__] : 1; + sj = __contains(weight, __, s2[j-1]) ? weight[__][s2[j-1]] : 1; + sij = __contains(weight, s1[i-1], s2[j-1]) ? weight[s1[i-1]][s2[j-1]] : (s1[i-1] == s2[j-1] ? 0 : 1); + if (dp[i-1][j-1] + sij <= dp[i-1][j] + si && dp[i-1][j-1] + sij <= dp[i][j-1] + sj) { + dp[i][j] = dp[i-1][j-1] + sij; + direct[i][j] = 'm'; + } + else if (dp[i-1][j] + si <= dp[i][j-1] + sj) { + dp[i][j] = dp[i-1][j] + si; + direct[i][j] = 's'; + } + else { + dp[i][j] = dp[i][j-1] + sj; + direct[i][j] = 'r'; + } + } + } + + for (i = m, j = n; i > 0 && j > 0; ){ + if (direct[i][j] == 'm') { + res[i-1] = j-1; + i--; j--; + } + else if (direct[i][j] == 's') i--; + else if (direct[i][j] == 'r') j--; + } + return res; +} + + vector levenshtein_distance_of_list(const string &str1, vector &str_list){ int size = str_list.size(); vector ls(size); @@ -174,36 +328,31 @@ vector levenshtein_distance_of_list(const string &str1, vector &str namespace py = pybind11; -PYBIND11_MODULE(pylcs, m) { - m.def("lcs", &lcs, R"pbdoc( - Longest common subsequence - )pbdoc"); - - m.def("lcs_of_list", &lcs_of_list, R"pbdoc( - Longest common subsequence of list - )pbdoc"); - - m.def("lcs2", &lcs2, R"pbdoc( - Longest common substring - )pbdoc"); - - m.def("lcs2_of_list", &lcs2_of_list, R"pbdoc( - Longest common substring of list - )pbdoc"); - - m.def("levenshtein_distance", &levenshtein_distance, R"pbdoc( - Levenshtein Distance of Two Strings - )pbdoc"); - - m.def("edit_distance", &levenshtein_distance, R"pbdoc( - Same As levenshtein_distance(): Levenshtein Distance of Two Strings - )pbdoc"); - - m.def("levenshtein_distance_of_list", &levenshtein_distance_of_list, R"pbdoc( - Levenshtein Distance of one string to a list of strings - )pbdoc"); - - m.def("edit_distance_of_list", &levenshtein_distance_of_list, R"pbdoc( - Levenshtein Distance of one string to a list of strings - )pbdoc"); +PYBIND11_MODULE(_pylcs, m) { + m.def("lcs", &lcs_sequence_length, R"pbdoc(Longest common subsequence)pbdoc"); + m.def("lcs_sequence_length", &lcs_sequence_length, R"pbdoc(Longest common subsequence)pbdoc"); + m.def("lcs_sequence_idx", &lcs_sequence_idx, R"pbdoc(Longest common subsequence indices mapping from str to ref)pbdoc", + py::arg("s"), py::arg("ref")); + m.def("lcs_of_list", &lcs_sequence_of_list, R"pbdoc(Longest common subsequence of list)pbdoc"); + m.def("lcs_sequence_of_list", &lcs_sequence_of_list, R"pbdoc(Longest common subsequence of list)pbdoc"); + + m.def("lcs2", &lcs_string_length, R"pbdoc(Longest common substring)pbdoc"); + m.def("lcs_string_length", &lcs_string_length, R"pbdoc(Longest common substring)pbdoc"); + m.def("lcs_string_idx", &lcs_string_idx, R"pbdoc(Longest common substring indices mapping from str to ref)pbdoc", + py::arg("s"), py::arg("ref")); + m.def("lcs2_of_list", &lcs_string_of_list, R"pbdoc(Longest common substring of list)pbdoc"); + m.def("lcs_string_of_list", &lcs_string_of_list, R"pbdoc(Longest common substring of list)pbdoc"); + + m.def("levenshtein_distance", &levenshtein_distance, R"pbdoc(Levenshtein Distance of Two Strings)pbdoc"); + m.def("levenshtein_distance", &levenshtein_distance_weighted, R"pbdoc(Levenshtein Distance of Two Strings. A weight dict> can be used.)pbdoc", + py::arg("str1"), py::arg("str2"), py::arg("weight")); + m.def("levenshtein_distance_idx", &levenshtein_distance_idx_weighted, R"pbdoc(Levenshtein Distance indices mapping from str to ref)pbdoc", + py::arg("s"), py::arg("ref"), py::arg("weight")=map>()); + m.def("edit_distance", &levenshtein_distance, R"pbdoc(Same As levenshtein_distance(): Levenshtein Distance of Two Strings)pbdoc"); + m.def("edit_distance", &levenshtein_distance_weighted, R"pbdoc(Same As levenshtein_distance(): Levenshtein Distance of Two Strings)pbdoc", + py::arg("str1"), py::arg("str2"), py::arg("weight")); + m.def("edit_distance_idx", &levenshtein_distance_idx_weighted, R"pbdoc(Edit Distance indices mapping from str to ref)pbdoc", + py::arg("s"), py::arg("ref"), py::arg("weight")=map>()); + m.def("levenshtein_distance_of_list", &levenshtein_distance_of_list, R"pbdoc(Levenshtein Distance of one string to a list of strings)pbdoc"); + m.def("edit_distance_of_list", &levenshtein_distance_of_list, R"pbdoc(Levenshtein Distance of one string to a list of strings)pbdoc"); } diff --git a/tests/coloring_match_sequence.jpg b/tests/coloring_match_sequence.jpg new file mode 100644 index 0000000..a773ccd Binary files /dev/null and b/tests/coloring_match_sequence.jpg differ diff --git a/tests/coloring_match_sequence_2.jpg b/tests/coloring_match_sequence_2.jpg new file mode 100644 index 0000000..33ebfbb Binary files /dev/null and b/tests/coloring_match_sequence_2.jpg differ diff --git a/tests/test_pylcs.py b/tests/test_pylcs.py index 132bf27..9f6a03c 100644 --- a/tests/test_pylcs.py +++ b/tests/test_pylcs.py @@ -1,39 +1,127 @@ # encoding=utf-8 import pylcs +import colorama -def test_lcs(): +def test_lcs_sequence(): + assert pylcs.lcs("aaa", "") == 0 + assert pylcs.lcs("", "bbb") == 0 assert pylcs.lcs("aaa", "bbb") == 0 - assert pylcs.lcs("aaa", "aabbbaa") == 3 assert pylcs.lcs("你好", "中国") == 0 - assert pylcs.lcs("aaa你好", "你好呀") == 2 + assert pylcs.lcs_sequence_length("aaa", "aabbbaa") == 3 + assert pylcs.lcs_sequence_length("aaa你好", "你好呀") == 2 -def test_lcs_of_list(): +def test_lcs_sequence_of_list(): assert pylcs.lcs_of_list("aaa", ["aabbbaa"] * 10) == [3] * 10 assert pylcs.lcs_of_list("aaa你好", ["你好呀"] * 10) == [2] * 10 + assert pylcs.lcs_sequence_of_list("aaa", ["aabbbaa"] * 10) == [3] * 10 + assert pylcs.lcs_sequence_of_list("aaa你好", ["你好呀"] * 10) == [2] * 10 -def test_lcs2(): +def test_lcs_sequence_idx(): + assert pylcs.lcs_sequence_idx("", "bbb") == [] + assert pylcs.lcs_sequence_idx("aaa", "") == [-1, -1, -1] + assert pylcs.lcs_sequence_idx("aaa", "bbb") == [-1, -1, -1] + assert pylcs.lcs_sequence_idx("你好", "中国") == [-1, -1] + res = pylcs.lcs_sequence_idx("aaa", "aabbbaa") + assert all([x in [0, 1, 5, 6] for x in res]) and res == sorted(res) + assert pylcs.lcs_sequence_idx("aaa你好", "你好呀") == [-1, -1, -1, 0, 1] + + +def test_lcs_string(): + assert pylcs.lcs2("aaa", "") == 0 + assert pylcs.lcs2("", "bbb") == 0 assert pylcs.lcs2("aaa", "bbb") == 0 - assert pylcs.lcs2("aaa", "aabbbaa") == 2 assert pylcs.lcs2("你好", "中国") == 0 - assert pylcs.lcs2("aaa你好", "好呀你") == 1 + assert pylcs.lcs_string_length("aaa", "aabbbaa") == 2 + assert pylcs.lcs_string_length("aaa你好", "好呀你") == 1 -def test_lcs2_of_list(): +def test_lcs_string_of_list(): assert pylcs.lcs2_of_list("aaa", ["aabbbaa"] * 10) == [2] * 10 assert pylcs.lcs2_of_list("aaa你好", ["好呀你"] * 10) == [1] * 10 + assert pylcs.lcs_string_of_list("aaa", ["aabbbaa"] * 10) == [2] * 10 + assert pylcs.lcs_string_of_list("aaa你好", ["好呀你"] * 10) == [1] * 10 + + +def test_lcs_string_idx(): + assert pylcs.lcs_string_idx("", "bbb") == [] + assert pylcs.lcs_string_idx("aaa", "") == [-1, -1, -1] + assert pylcs.lcs_string_idx("aaa", "bbb") == [-1, -1, -1] + assert pylcs.lcs_string_idx("你好", "中国") == [-1, -1] + assert pylcs.lcs_string_idx("aaa", "aabbbaa") in ([0, 1, -1], [-1, 0, 1], [5, 6, -1], [-1, 5, 6]) + assert pylcs.lcs_string_idx("aaa", "aabbbaaa") == [5, 6, 7] + assert pylcs.lcs_string_idx("aaa你好", "你好呀") == [-1, -1, -1, 0, 1] def test_edit_distance(): + assert pylcs.edit_distance("", "") == 0 + assert pylcs.edit_distance("", "bbb") == 3 + assert pylcs.edit_distance("aaa", "") == 3 + assert pylcs.edit_distance("aaa", "aaa") == 0 assert pylcs.edit_distance("aaa", "bbb") == 3 assert pylcs.edit_distance("aaa", "aabbbaa") == 4 assert pylcs.edit_distance("你好", "中国") == 2 assert pylcs.edit_distance("aaa你好", "你好呀") == 4 +def test_edit_distance_weighted(): + assert pylcs.edit_distance("", "bbb", {}) == 3 + assert pylcs.edit_distance("", "bbb", {'': {'b': 2}}) == 6 + assert pylcs.edit_distance("", "bbb", {'b': {'': 2}}) == 3 + assert pylcs.edit_distance("aaa", "", {}) == 3 + assert pylcs.edit_distance("aaa", "", {'': {'a': 2}}) == 3 + assert pylcs.edit_distance("aaa", "", {'a': {'': 2}}) == 6 + assert pylcs.edit_distance("aaa", "aba", {'a': {'b': 2}}) == 2 + assert pylcs.edit_distance("aaa", "aba", {'a': {'b': 3}}) == 2 + assert pylcs.edit_distance("aaa", "aaa", {'a': {'a': 1}}) == 3 + assert pylcs.edit_distance("aa", "aababb", {'a': {'a': 0.5, 'b': 2}}) == 5 + assert pylcs.edit_distance("x你好", "你好呀", {'你': {'你': -1}}) == 1 + assert pylcs.edit_distance("x你好", "你好呀", {'你好': {'你好': -1}}) == 2 + weight = {'A': {'A': -2, 'C': 7, 'G': 5, 'T': 7, '': 5}, + 'C': {'A': 7, 'C': -2, 'G': 7, 'T': 5, '': 5}, + 'G': {'A': 5, 'C': 7, 'G': -2, 'T': 7, '': 5}, + 'T': {'A': 7, 'C': 5, 'G': 7, 'T': -2, '': 5}, + '': {'A': 5, 'C': 5, 'G': 5, 'T': 5, '': 0}} + assert pylcs.edit_distance("GCT", "CTT", weight) == 6 + + +def test_edit_distance_idx(): + assert pylcs.edit_distance_idx("aaa", "") == [-1, -1, -1] + assert pylcs.edit_distance_idx("", "bbb") == [] + assert pylcs.edit_distance_idx("aaa", "aba", {'a': {'b': 1}}) == [0, 1, 2] + assert pylcs.edit_distance_idx("aaa", "aba", {'a': {'b': 3}}) == [0, -1, 2] + assert pylcs.edit_distance_idx("aa", "aabb", {'a': {'a': 2, 'b': 0}}) == [2, 3] + + def test_edit_distance_of_list(): assert pylcs.edit_distance_of_list("aaa", ["bbb"] * 10) == [3] * 10 assert pylcs.edit_distance_of_list("aaa你好", ["你好呀"] * 10) == [4] * 10 assert pylcs.edit_distance_of_list("aaa你好", ["bbb", "你好呀"] * 10) == [5, 4] * 10 + + +def test_coloring_match_sequence(): + s1, s2 = "abcdefghijklmnopq", "-c-fgh-kl-nop-q" + match_list = pylcs.lcs_sequence_idx(s1, s2) + colored_s1, colored_s2 = pylcs.coloring_match_sequence(match_list, s1, s2, 11, 11, "#2266ff", "#2266ff", t=1) + print(colored_s1, colored_s2) + colored_s1, colored_s2 = pylcs.coloring_match_sequence(match_list, s1, s2, 11, 11, "#2266ff", "#2266ff", t=2) + print(colored_s1, colored_s2) + colored_s1, colored_s2 = pylcs.coloring_match_sequence(match_list, s1, s2, 11, 11, "#2266ff", "#2266ff", t=3) + print(colored_s1, colored_s2) + + +test_lcs_sequence() +test_lcs_sequence_of_list() +test_lcs_sequence_idx() +test_lcs_string() +test_lcs_string_of_list() +test_lcs_string_idx() +test_edit_distance() +test_edit_distance_weighted() +test_edit_distance_idx() +test_edit_distance_of_list() + +colorama.init(autoreset=True) +test_coloring_match_sequence()