diff --git a/experiments/comprehensive_test.py b/experiments/comprehensive_test.py new file mode 100644 index 00000000..aa81e513 --- /dev/null +++ b/experiments/comprehensive_test.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Comprehensive test showing the fix for issue #54: "top Visual Basic is not working" + +This test demonstrates: +1. The original problem with Visual Basic command parsing +2. How the fix resolves the issue +3. Validation that other language combinations still work +""" + +import re +from typing import List + +# Simplified language list for testing (based on the actual config) +DEFAULT_PROGRAMMING_LANGUAGES = [ + r"Visual Basic", + r"JavaScript", + r"TypeScript", + r"Java", + r"Python", + r"C\+\+", + r"C", + r"C#", + r"Go", + r"Basic" # Note: this is different from "Visual Basic" +] + +def get_default_programming_language(language: str) -> str: + """Returns default appearance of language (mimics the original utility)""" + language = language.lower() + for lang in DEFAULT_PROGRAMMING_LANGUAGES: + if lang.replace('\\', '').lower() == language: + return lang + return "" + +def old_parse_method(text: str) -> List[str]: + """The old method that was causing the bug""" + return re.split(r'\s+', text) + +def new_parse_method(text: str) -> List[str]: + """The new fixed method""" + if not text: + return [] + + # Get all language names without regex escaping + language_names = [] + for lang_pattern in DEFAULT_PROGRAMMING_LANGUAGES: + lang_name = (lang_pattern.replace('\\+', '+') + .replace('\\-', '-') + .replace('\\#', '#') + .replace('\\!', '!') + .replace('\\', '')) + language_names.append(lang_name) + + # Sort by length (longest first) to prioritize multi-word languages + language_names.sort(key=len, reverse=True) + + matched_languages = [] + remaining_text = text + + for lang_name in language_names: + pattern = r'\b' + re.escape(lang_name) + r'\b' + match = re.search(pattern, remaining_text, re.IGNORECASE) + + if match: + canonical_name = get_default_programming_language(lang_name) + if canonical_name: + clean_name = (canonical_name.replace('\\+', '+') + .replace('\\-', '-') + .replace('\\#', '#') + .replace('\\!', '!') + .replace('\\', '')) + if clean_name not in matched_languages: + matched_languages.append(clean_name) + remaining_text = remaining_text[:match.start()] + remaining_text[match.end():] + + return matched_languages + +def contains_all_strings(user_languages: List[str], search_languages: List[str], ignore_case: bool) -> bool: + """Mimics the contains_all_strings function from utils""" + if not search_languages: + return True + + for search_lang in search_languages: + found = False + for user_lang in user_languages: + if ignore_case: + if user_lang.lower() == search_lang.lower(): + found = True + break + else: + if user_lang == search_lang: + found = True + break + if not found: + return False + return True + +def test_issue_54_fix(): + """Test that demonstrates the fix for issue #54""" + print("=" * 60) + print("COMPREHENSIVE TEST FOR ISSUE #54: 'top Visual Basic is not working'") + print("=" * 60) + print() + + # Test case from the issue + issue_command = "Visual Basic" + + print("1. REPRODUCING THE ORIGINAL BUG") + print("-" * 40) + print(f"User command: 'top {issue_command}'") + print(f"Extracted languages text: '{issue_command}'") + print() + + old_result = old_parse_method(issue_command) + print(f"OLD METHOD (buggy): {old_result}") + print(" -> Searches for users with 'Visual' AND 'Basic' languages") + print(" -> This is WRONG - 'Visual Basic' is one language!") + print() + + new_result = new_parse_method(issue_command) + print(f"NEW METHOD (fixed): {new_result}") + print(" -> Searches for users with 'Visual Basic' language") + print(" -> This is CORRECT!") + print() + + print("2. SIMULATING USER DATABASE SEARCH") + print("-" * 40) + + # Simulate some user data + mock_users = [ + {"name": "Alice", "programming_languages": ["Python", "Java"]}, + {"name": "Bob", "programming_languages": ["Visual Basic", "C#"]}, + {"name": "Charlie", "programming_languages": ["JavaScript", "TypeScript"]}, + {"name": "Diana", "programming_languages": ["Visual", "Basic"]}, # Someone with separate "Visual" and "Basic" languages + ] + + print("Mock user database:") + for user in mock_users: + print(f" {user['name']}: {user['programming_languages']}") + print() + + print("Search results for 'top Visual Basic':") + + # Test with old method + old_matches = [user for user in mock_users if contains_all_strings(user['programming_languages'], old_result, True)] + print(f"OLD METHOD finds: {[u['name'] for u in old_matches]}") + print(" -> Diana has both 'Visual' and 'Basic' as separate languages") + print(" -> Bob has 'Visual Basic' as one language, but doesn't match!") + print() + + # Test with new method + new_matches = [user for user in mock_users if contains_all_strings(user['programming_languages'], new_result, True)] + print(f"NEW METHOD finds: {[u['name'] for u in new_matches]}") + print(" -> Bob has 'Visual Basic' language - CORRECT match!") + print(" -> Diana doesn't match - she doesn't have 'Visual Basic' as one language") + print() + + print("3. TESTING OTHER LANGUAGE COMBINATIONS") + print("-" * 40) + + other_tests = [ + "Python Java", + "JavaScript TypeScript", + "Visual Basic Python", + "Python" + ] + + for test_case in other_tests: + old_result = old_parse_method(test_case) + new_result = new_parse_method(test_case) + status = "✓ SAME" if old_result == new_result else "⚠ DIFFERENT" + print(f"'{test_case}':") + print(f" OLD: {old_result}") + print(f" NEW: {new_result} {status}") + print() + + print("4. CONCLUSION") + print("-" * 40) + print("✓ The fix correctly handles 'Visual Basic' as a single language") + print("✓ Other language combinations continue to work as expected") + print("✓ Issue #54 is RESOLVED!") + print() + +if __name__ == "__main__": + test_issue_54_fix() \ No newline at end of file diff --git a/experiments/fix_language_parsing.py b/experiments/fix_language_parsing.py new file mode 100644 index 00000000..4e37fa0c --- /dev/null +++ b/experiments/fix_language_parsing.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Test the improved language parsing logic.""" + +import sys +import os +import re +from typing import List + +# Add the parent directory to sys.path to import modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python')) + +from config import DEFAULT_PROGRAMMING_LANGUAGES + +def parse_languages_from_text(text: str) -> List[str]: + """ + Parse language names from text, handling multi-word languages like 'Visual Basic'. + + :param text: Input text containing language names + :return: List of matched language names + """ + # Convert regex patterns back to actual language names for matching + language_names = [] + for lang_pattern in DEFAULT_PROGRAMMING_LANGUAGES: + # Remove regex escaping to get actual language name + lang_name = lang_pattern.replace('\\', '') + language_names.append(lang_name) + + # Sort by length (longest first) to match multi-word languages first + language_names.sort(key=len, reverse=True) + + matched_languages = [] + remaining_text = text + + # Case-insensitive matching + for lang_name in language_names: + # Use word boundaries and case-insensitive matching + pattern = r'\b' + re.escape(lang_name) + r'\b' + if re.search(pattern, remaining_text, re.IGNORECASE): + matched_languages.append(lang_name) + # Remove the matched language from remaining text to avoid duplicates + remaining_text = re.sub(pattern, '', remaining_text, flags=re.IGNORECASE) + + return matched_languages + +def test_improved_parsing(): + """Test the improved language parsing""" + test_cases = [ + "Visual Basic", + "visual basic", + "Python Java", + "Visual Basic C#", + "JavaScript TypeScript Python", + "C++ C# Java", + "Go Python", + "Objective-C C++", + "F# C#" + ] + + print("Testing improved language parsing:") + for case in test_cases: + result = parse_languages_from_text(case) + print(f"'{case}' -> {result}") + +if __name__ == "__main__": + test_improved_parsing() \ No newline at end of file diff --git a/experiments/fix_language_parsing_v2.py b/experiments/fix_language_parsing_v2.py new file mode 100644 index 00000000..1a8a5c68 --- /dev/null +++ b/experiments/fix_language_parsing_v2.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Test the improved language parsing logic v2.""" + +import sys +import os +import re +from typing import List + +# Add the parent directory to sys.path to import modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python')) + +from config import DEFAULT_PROGRAMMING_LANGUAGES + +def parse_languages_from_text(text: str) -> List[str]: + """ + Parse language names from text, handling multi-word languages like 'Visual Basic'. + + :param text: Input text containing language names + :return: List of matched language names + """ + # Convert regex patterns back to actual language names for matching + language_names = [] + for lang_pattern in DEFAULT_PROGRAMMING_LANGUAGES: + # Remove regex escaping to get actual language name + lang_name = lang_pattern.replace('\\+', '+').replace('\\-', '-').replace('\\#', '#').replace('\\!', '!') + language_names.append(lang_name) + + # Sort by length (longest first) to match multi-word languages first + language_names.sort(key=len, reverse=True) + + matched_languages = [] + remaining_text = text + + # Case-insensitive matching + for lang_name in language_names: + # Use word boundaries and case-insensitive matching + # For special characters, we need to be more careful with escaping + escaped_name = re.escape(lang_name) + pattern = r'\b' + escaped_name + r'\b' + + if re.search(pattern, remaining_text, re.IGNORECASE): + matched_languages.append(lang_name) + # Remove the matched language from remaining text to avoid duplicates + remaining_text = re.sub(pattern, '', remaining_text, flags=re.IGNORECASE) + remaining_text = re.sub(r'\s+', ' ', remaining_text).strip() # Clean up extra spaces + + return matched_languages + +def test_improved_parsing_v2(): + """Test the improved language parsing v2""" + test_cases = [ + "Visual Basic", + "visual basic", + "Python Java", + "Visual Basic C#", + "JavaScript TypeScript Python", + "C++ C# Java", + "Go Python", + "Objective-C C++", + "F# C#" + ] + + print("Testing improved language parsing v2:") + for case in test_cases: + result = parse_languages_from_text(case) + print(f"'{case}' -> {result}") + +if __name__ == "__main__": + test_improved_parsing_v2() \ No newline at end of file diff --git a/experiments/fix_language_parsing_v3.py b/experiments/fix_language_parsing_v3.py new file mode 100644 index 00000000..135eb0b7 --- /dev/null +++ b/experiments/fix_language_parsing_v3.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Test the improved language parsing logic v3 using existing utility.""" + +import sys +import os +import re +from typing import List + +# Add the parent directory to sys.path to import modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python')) + +from config import DEFAULT_PROGRAMMING_LANGUAGES +from modules.utils import get_default_programming_language + +def parse_languages_from_text(text: str) -> List[str]: + """ + Parse language names from text, handling multi-word languages like 'Visual Basic'. + Uses the existing get_default_programming_language utility for accurate matching. + + :param text: Input text containing language names + :return: List of matched language names + """ + # Get all possible language names (without regex escaping) + language_names = [] + for lang_pattern in DEFAULT_PROGRAMMING_LANGUAGES: + # Remove common regex escaping to get actual language name + lang_name = lang_pattern.replace('\\+', '+').replace('\\-', '-').replace('\\#', '#').replace('\\!', '!').replace('\\', '') + language_names.append(lang_name) + + # Sort by length (longest first) to match multi-word languages first + language_names.sort(key=len, reverse=True) + + matched_languages = [] + remaining_text = text.lower() + + # Try to find each language in the text + for lang_name in language_names: + # Check if this language appears in the text (case-insensitive) + if lang_name.lower() in remaining_text: + # Use word boundaries to ensure we match complete words + pattern = r'\b' + re.escape(lang_name.lower()) + r'\b' + if re.search(pattern, remaining_text): + # Use the utility function to get the correct case + correct_name = get_default_programming_language(lang_name) + if correct_name and correct_name not in [ml.replace('\\', '') for ml in matched_languages]: + matched_languages.append(correct_name.replace('\\', '')) + # Remove the matched language from remaining text + remaining_text = re.sub(pattern, '', remaining_text) + remaining_text = re.sub(r'\s+', ' ', remaining_text).strip() + + return matched_languages + +def test_improved_parsing_v3(): + """Test the improved language parsing v3""" + test_cases = [ + "Visual Basic", + "visual basic", + "Python Java", + "Visual Basic C#", + "JavaScript TypeScript Python", + "C++ C# Java", + "Go Python", + "Objective-C C++", + "F# C#", + "c++ java python", + "VISUAL BASIC python" + ] + + print("Testing improved language parsing v3:") + for case in test_cases: + result = parse_languages_from_text(case) + print(f"'{case}' -> {result}") + + # Test the utility function directly + print(f"\nTesting get_default_programming_language:") + print(f"'visual basic' -> '{get_default_programming_language('visual basic')}'") + print(f"'c++' -> '{get_default_programming_language('c++')}'") + print(f"'c#' -> '{get_default_programming_language('c#')}'") + +if __name__ == "__main__": + test_improved_parsing_v3() \ No newline at end of file diff --git a/experiments/simple_fix_test.py b/experiments/simple_fix_test.py new file mode 100644 index 00000000..af55add9 --- /dev/null +++ b/experiments/simple_fix_test.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Simple test of the language parsing fix.""" + +import re +from typing import List + +# Simplified list of languages for testing +DEFAULT_LANGUAGES = [ + r"Visual Basic", + r"JavaScript", + r"TypeScript", + r"Java", + r"Python", + r"C\+\+", + r"C", + r"C#", + r"Objective\-C", + r"Go", + r"F#" +] + +def get_default_programming_language(language: str) -> str: + """Returns default appearance of language""" + language = language.lower() + for lang in DEFAULT_LANGUAGES: + if lang.replace('\\', '').lower() == language: + return lang.replace('\\', '') + return "" + +def parse_languages_from_text(text: str) -> List[str]: + """ + Parse language names from text, handling multi-word languages like 'Visual Basic'. + + :param text: Input text containing language names + :return: List of matched language names + """ + # Get all possible language names (without regex escaping) + language_names = [] + for lang_pattern in DEFAULT_LANGUAGES: + # Remove common regex escaping to get actual language name + lang_name = lang_pattern.replace('\\+', '+').replace('\\-', '-').replace('\\#', '#') + language_names.append(lang_name) + + # Sort by length (longest first) to match multi-word languages first + language_names.sort(key=len, reverse=True) + + matched_languages = [] + remaining_text = text.lower() + + # Try to find each language in the text + for lang_name in language_names: + # Check if this language appears in the text (case-insensitive) + lang_lower = lang_name.lower() + if lang_lower in remaining_text: + # Use word boundaries to ensure we match complete words + pattern = r'\b' + re.escape(lang_lower) + r'\b' + if re.search(pattern, remaining_text): + # Get the correct case using the utility function + correct_name = get_default_programming_language(lang_name) + if correct_name and correct_name not in matched_languages: + matched_languages.append(correct_name) + # Remove the matched language from remaining text + remaining_text = re.sub(pattern, '', remaining_text) + remaining_text = re.sub(r'\s+', ' ', remaining_text).strip() + + return matched_languages + +def test_parsing(): + """Test the language parsing""" + test_cases = [ + "Visual Basic", + "visual basic", + "Python Java", + "Visual Basic C#", + "JavaScript TypeScript Python", + "C++ C# Java", + "Go Python", + "Objective-C C++", + "F# C#" + ] + + print("Testing language parsing:") + for case in test_cases: + result = parse_languages_from_text(case) + print(f"'{case}' -> {result}") + +if __name__ == "__main__": + test_parsing() \ No newline at end of file diff --git a/experiments/test_fix.py b/experiments/test_fix.py new file mode 100644 index 00000000..b3cf4a02 --- /dev/null +++ b/experiments/test_fix.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Test the fix for Visual Basic parsing.""" + +import sys +import os + +# Add the python directory to sys.path to import modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python')) + +# Test only the new function without importing the full module +from typing import List +import re + +DEFAULT_PROGRAMMING_LANGUAGES = [ + r"Assembler", + r"JavaScript", + r"TypeScript", + r"Java", + r"Python", + r"PHP", + r"Ruby", + r"C\+\+", + r"C", + r"Shell", + r"C#", + r"Objective\-C", + r"R", + r"VimL", + r"Go", + r"Perl", + r"CoffeeScript", + r"TeX", + r"Swift", + r"Kotlin", + r"F#", + r"Scala", + r"Scheme", + r"Emacs Lisp", + r"Lisp", + r"Haskell", + r"Lua", + r"Clojure", + r"TLA\+", + r"PlusCal", + r"Matlab", + r"Groovy", + r"Puppet", + r"Rust", + r"PowerShell", + r"Pascal", + r"Delphi", + r"SQL", + r"Nim", + r"1С", + r"КуМир", + r"Scratch", + r"Prolog", + r"GLSL", + r"HLSL", + r"Whitespace", + r"Basic", + r"Visual Basic", + r"Parser", + r"Erlang", + r"Wolfram", + r"Brainfuck", + r"Pawn", + r"Cobol", + r"Fortran", + r"Arduino", + r"Makefile", + r"CMake", + r"D", + r"Forth", + r"Dart", + r"Ada", + r"Julia", + r"Malbolge", + r"Лого", + r"Verilog", + r"VHDL", + r"Altera", + r"Processing", + r"MetaQuotes", + r"Algol", + r"Piet", + r"Shakespeare", + r"G\-code", + r"Whirl", + r"Chef", + r"BIT", + r"Ook", + r"MoonScript", + r"PureScript", + r"Idris", + r"Elm", + r"Minecraft", + r"Crystal", + r"C\-\-", + r"Go\!", + r"Tcl", + r"Solidity", + r"AssemblyScript", + r"Vimscript", + r"Pony", + r"LOLCODE", + r"Elixir", + r"X#", + r"NVPTX", + r"Nemerle", +] + +def get_default_programming_language(language: str) -> str: + """Returns default appearance of language""" + language = language.lower() + for lang in DEFAULT_PROGRAMMING_LANGUAGES: + if lang.replace('\\', '').lower() == language: + return lang + return "" + +def parse_programming_languages(text: str) -> List[str]: + """Parse programming language names from text, handling multi-word languages.""" + if not text: + return [] + + # Get all language names without regex escaping + language_names = [] + for lang_pattern in DEFAULT_PROGRAMMING_LANGUAGES: + # Remove regex escaping to get actual language name + lang_name = (lang_pattern.replace('\\+', '+') + .replace('\\-', '-') + .replace('\\#', '#') + .replace('\\!', '!') + .replace('\\', '')) + language_names.append(lang_name) + + # Sort by length (longest first) to prioritize multi-word languages + language_names.sort(key=len, reverse=True) + + matched_languages = [] + remaining_text = text + + # Find each language in the text (case-insensitive) + for lang_name in language_names: + # Check if this language appears in the remaining text + pattern = r'\b' + re.escape(lang_name) + r'\b' + match = re.search(pattern, remaining_text, re.IGNORECASE) + + if match: + # Use the utility function to get the correct canonical form + canonical_name = get_default_programming_language(lang_name) + if canonical_name: + # Remove regex escaping from the canonical name for return + clean_name = (canonical_name.replace('\\+', '+') + .replace('\\-', '-') + .replace('\\#', '#') + .replace('\\!', '!') + .replace('\\', '')) + if clean_name not in matched_languages: + matched_languages.append(clean_name) + # Remove the matched text to avoid overlapping matches + remaining_text = remaining_text[:match.start()] + remaining_text[match.end():] + + return matched_languages + +def test_fix(): + """Test the fix""" + print("Testing the fix for Visual Basic parsing:") + print() + + # Test cases that should work with the fix + test_cases = [ + "Visual Basic", + "visual basic", + "Python Java", + "Visual Basic C#", + "JavaScript TypeScript", + "C++ Java" + ] + + print("=== NEW BEHAVIOR (with fix) ===") + for case in test_cases: + result = parse_programming_languages(case) + print(f"'{case}' -> {result}") + + print() + print("=== OLD BEHAVIOR (without fix) ===") + # Simulate old behavior + for case in test_cases: + old_result = re.split(r'\s+', case) + print(f"'{case}' -> {old_result}") + +if __name__ == "__main__": + test_fix() \ No newline at end of file diff --git a/experiments/test_language_parsing.py b/experiments/test_language_parsing.py new file mode 100644 index 00000000..5c1cd0a6 --- /dev/null +++ b/experiments/test_language_parsing.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Test script to demonstrate the language parsing issue.""" + +import sys +import os +from regex import split + +# Add the parent directory to sys.path to import modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python')) + +from config import DEFAULT_PROGRAMMING_LANGUAGES + +def test_current_behavior(): + """Test how the current split logic works with Visual Basic""" + + # Simulate what happens when someone types "top Visual Basic" + matched_languages = "Visual Basic" + + print("Current behavior:") + print(f"Input: '{matched_languages}'") + + # This is what the current code does + languages = split(r"\s+", matched_languages) + print(f"After split: {languages}") + + # Show what the actual language names are in the config + print(f"\nActual Visual Basic in config: {[lang for lang in DEFAULT_PROGRAMMING_LANGUAGES if 'Visual' in lang]}") + +def test_multiple_languages(): + """Test how it should work with multiple languages""" + + test_cases = [ + "Python Java", + "Visual Basic C#", + "JavaScript TypeScript Python" + ] + + print("\nTesting multiple language parsing:") + for case in test_cases: + languages = split(r"\s+", case) + print(f"'{case}' -> {languages}") + +if __name__ == "__main__": + test_current_behavior() + test_multiple_languages() \ No newline at end of file diff --git a/experiments/test_visual_basic_pattern.py b/experiments/test_visual_basic_pattern.py new file mode 100644 index 00000000..99d763b5 --- /dev/null +++ b/experiments/test_visual_basic_pattern.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Test script to reproduce the Visual Basic pattern matching issue.""" + +import sys +import os + +# Add the parent directory to sys.path to import modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python')) + +from regex import compile as recompile, IGNORECASE +from config import DEFAULT_PROGRAMMING_LANGUAGES_PATTERN_STRING as DEFAULT_LANGUAGES + +# Recreate the patterns from patterns.py +TOP = recompile( + r'\A\s*(топ|верх|top)\s*(?P\d+)?\s*\Z', IGNORECASE) + +TOP_LANGUAGES = recompile( + r'\A\s*(топ|верх|top)\s*(?P\d+\s+)?\s*(?P(' + DEFAULT_LANGUAGES + + r')(\s+(' + DEFAULT_LANGUAGES + r'))*)\s*\Z', IGNORECASE) + +PEOPLE_LANGUAGES = recompile( + r'\A\s*(люди|народ|people)\s*(?P(' + DEFAULT_LANGUAGES + + r')(\s+(' + DEFAULT_LANGUAGES + r'))*)\s*\Z', IGNORECASE) + +def test_patterns(): + """Test various Visual Basic related commands.""" + test_cases = [ + "top Visual Basic", + "top visual basic", + "TOP Visual Basic", + "топ Visual Basic", + "верх Visual Basic", + "top 5 Visual Basic", + "top Visual Basic 5", + "people Visual Basic", + "люди Visual Basic", + "народ Visual Basic", + ] + + print("Testing TOP pattern:") + for case in test_cases: + match = TOP.match(case) + print(f" '{case}' -> {bool(match)}") + if match: + print(f" Groups: {match.groupdict()}") + + print("\nTesting TOP_LANGUAGES pattern:") + for case in test_cases: + match = TOP_LANGUAGES.match(case) + print(f" '{case}' -> {bool(match)}") + if match: + print(f" Groups: {match.groupdict()}") + + print("\nTesting PEOPLE_LANGUAGES pattern:") + for case in test_cases: + match = PEOPLE_LANGUAGES.match(case) + print(f" '{case}' -> {bool(match)}") + if match: + print(f" Groups: {match.groupdict()}") + +if __name__ == "__main__": + test_patterns() \ No newline at end of file diff --git a/python/modules/commands.py b/python/modules/commands.py index 93d99817..a2fa6261 100644 --- a/python/modules/commands.py +++ b/python/modules/commands.py @@ -17,7 +17,8 @@ get_default_programming_language, contains_all_strings, karma_limit, - is_available_ghpage + is_available_ghpage, + parse_programming_languages ) import config import tokens @@ -148,7 +149,8 @@ def top_langs( """Sends users top.""" if self.peer_id < 2e9: return - languages = split(r"\s+", self.matched.group("languages")) + # Use the new parse_programming_languages function instead of simple split + languages = parse_programming_languages(self.matched.group("languages")) count = self.matched.group("count") users = DataBuilder.get_users_sorted_by_karma( self.vk_instance, self.data_service, self.peer_id) diff --git a/python/modules/utils.py b/python/modules/utils.py index e30e7c2c..b990c4d5 100644 --- a/python/modules/utils.py +++ b/python/modules/utils.py @@ -2,6 +2,7 @@ from typing import NoReturn, List import requests import config +import re def get_default_programming_language( @@ -68,3 +69,58 @@ def is_available_ghpage( """Returns True if github profile is available. """ return requests.get(f'https://github.com/{profile}').status_code == 200 + + +def parse_programming_languages( + text: str +) -> List[str]: + """Parse programming language names from text, handling multi-word languages. + + This function correctly handles languages like 'Visual Basic' that contain spaces, + which would be incorrectly split by a simple regex split. + + :param text: Input text containing language names + :return: List of matched language names in their canonical form + """ + if not text: + return [] + + # Get all language names without regex escaping + language_names = [] + for lang_pattern in config.DEFAULT_PROGRAMMING_LANGUAGES: + # Remove regex escaping to get actual language name + lang_name = (lang_pattern.replace('\\+', '+') + .replace('\\-', '-') + .replace('\\#', '#') + .replace('\\!', '!') + .replace('\\', '')) + language_names.append(lang_name) + + # Sort by length (longest first) to prioritize multi-word languages + language_names.sort(key=len, reverse=True) + + matched_languages = [] + remaining_text = text + + # Find each language in the text (case-insensitive) + for lang_name in language_names: + # Check if this language appears in the remaining text + pattern = r'\b' + re.escape(lang_name) + r'\b' + match = re.search(pattern, remaining_text, re.IGNORECASE) + + if match: + # Use the utility function to get the correct canonical form + canonical_name = get_default_programming_language(lang_name) + if canonical_name: + # Remove regex escaping from the canonical name for return + clean_name = (canonical_name.replace('\\+', '+') + .replace('\\-', '-') + .replace('\\#', '#') + .replace('\\!', '!') + .replace('\\', '')) + if clean_name not in matched_languages: + matched_languages.append(clean_name) + # Remove the matched text to avoid overlapping matches + remaining_text = remaining_text[:match.start()] + remaining_text[match.end():] + + return matched_languages