diff --git a/soundchanger/change.py b/soundchanger/change.py index c4d1fcd..a9c7f79 100644 --- a/soundchanger/change.py +++ b/soundchanger/change.py @@ -1,7 +1,7 @@ import re -def apply(change, string, categories={}, apply=True): +def apply(change, string, categories={}, apply=True, zero_characters=['∅']): """Apply a sound change to a given string""" if not apply: @@ -10,14 +10,13 @@ def apply(change, string, categories={}, apply=True): # Check validity of change if change.count('>') != 1: raise ValueError(f"Change {change} is not a valid sound change. (Missing character '>')") - if change.count('/') > 1: raise ValueError(f"Change {change} is not a valid sound change. (More than one '/' character)") # Prepare change for regex - for k, v in {'{': '(', '}': ')', ',': '|'}.items(): + for k, v in {' ': '', '{': '(', '}': ')', ',': '|'}.items(): change = change.replace(k, v) - + # Replacements for categories for k, v in categories.items(): # Replacement of categories inside brackets (before or after a comma) @@ -30,15 +29,24 @@ def apply(change, string, categories={}, apply=True): else: environment = '_' + # Collaplse multiple underscores + environment = re.sub('(_+)', '_', environment) + if environment.count('_') != 1: raise ValueError(f"Environment {environment} is not a valid environment. (Character '_' should exist exactly once)") original, change_to = change.split('>') before, after = environment.split('_') - n = before.count('(') + original.count('(') + 3 + if not original: + raise ValueError(f"Nothing to change from.") + + # Remove zero characters + for char in zero_characters: + change_to = change_to.replace(char, '') + pattern = f"({before})({original})({after})" - repl = f"\\1{change_to}\\{n}" - - return re.sub(pattern, repl, f"#{string}#").strip('#') + last_group_index = pattern.count('(') - after.count('(') + replacement = f"\\1{change_to}\\{last_group_index}" + return re.sub(pattern, replacement, f"#{string}#").strip('#') diff --git a/tests/test_change.py b/tests/test_change.py index 8d594ad..fba8203 100644 --- a/tests/test_change.py +++ b/tests/test_change.py @@ -1,13 +1,43 @@ from soundchanger.change import apply -def test_apply_without_environment(): +def test_without_environment(): assert apply('p>h', 'pana') == 'hana' -def test_apply_with_environment(): +def test_with_environment(): assert apply('p>f/#_u', 'pune') == 'fune' -def test_apply_with_complex_environment(): +def test_with_partial_environment(): + assert apply('p>h/#_', 'pi') == 'hi' + assert apply('p>h/_i', 'pi') == 'hi' + +def test_change_to_nothing(): + assert apply('u>', 'fun') == 'fn' + +def test_change_to_nothing_with_environment(): + assert apply('u>/_n#', 'fun') == 'fn' + +def test_change_to_zero_character_with_environment(): + assert apply('w>∅', 'kawa') == 'kaa' + +def test_custom_zero_character(): + assert apply('w>-', 'kawa', zero_characters=['-']) == 'kaa' + +def test_whitespace_in_change(): + assert apply(' a > b / c _ d ', 'cad') == 'cbd' + +def test_multiple_underscores_in_environment(): + assert apply('a>b/c___d ', 'cad') == 'cbd' + +def test_categories_as_string(): + assert apply('V>o', 'ha', categories={'V': 'aiu'}) == 'ho' + assert apply('V>o', 'he', categories={'V': 'aiu'}) == 'he' + +def test_categories_as_list(): + assert apply('V>o', 'ha', categories={'V': ['a', 'i', 'u']}) == 'ho' + assert apply('V>o', 'he', categories={'V': ['a', 'i', 'u']}) == 'he' + +def test_complex_environment(): inputs = ['pana', 'pina', 'puna', 'pama', 'pima', 'puma'] outputs = ['pana', 'hina', 'huna', 'pama', 'hima', 'huma'] for string, output in zip(inputs, outputs): @@ -17,14 +47,14 @@ def test_apply_with_complex_environment(): categories={'N': 'nm'} ) == output -def test_apply_with_complex_group_and_categories(): +def test_complex_group_and_categories(): assert apply('e>i/{#,p,t,k}V{m,n,h}_#', 'pane', categories={ 'V': 'aiu', 'P': 'ptk', 'N': 'mn', }) == 'pani' -def test_apply_with_complex_group_and_categories_and_digraphs(): +def test_with_complex_group_and_categories_and_digraphs(): assert apply('u>o/#V{ts,pf,t}_k{V,e,o}#', 'atsuka', categories={ 'V': ['a', 'i', 'u'] }) == 'atsoka' @@ -34,3 +64,14 @@ def test_apply_with_complex_group_and_categories_and_digraphs(): assert apply('u>o/#V{ts,pf,t}_k{V,e,o}#', 'matsuka', categories={ 'V': ['a', 'i', 'u'] }) == 'matsuka' + +def test_multiple_changes(): + string = 'pana' + for change in ['p>f', 'n>m/fa_']: + string = apply(change, string) + assert string == 'fama' + +def test_change_from_category(): + assert apply('V>o', 'tatiru', categories={ + 'V': 'aiu' + }) == 'totoro' diff --git a/tests/test_change_errors.py b/tests/test_change_errors.py index 14fb14a..da6901c 100644 --- a/tests/test_change_errors.py +++ b/tests/test_change_errors.py @@ -2,14 +2,18 @@ import pytest import re from soundchanger.change import apply -def test_parse_change_error_change(): +def test_error_in_change(): with pytest.raises(ValueError, match=re.escape("Change is not a valid sound change. (Missing character '>')")): apply('', '') -def test_apply_error_split(): +def test_error_split(): with pytest.raises(ValueError, match=re.escape("Change a>b/c/d is not a valid sound change. (More than one '/' character)")): apply('a>b/c/d', '') -def test_apply_error_environment(): +def test_error_in_environment(): with pytest.raises(ValueError, match=re.escape("Environment c is not a valid environment. (Character '_' should exist exactly once)")): apply('a>b/c', '') + +def test_no_change_from(): + with pytest.raises(ValueError, match=re.escape("Nothing to change from.")): + apply('>b', '')