Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Camelize/decamelize asymmetries #37

Closed
mjschock opened this issue Nov 5, 2019 · 4 comments
Closed

Camelize/decamelize asymmetries #37

mjschock opened this issue Nov 5, 2019 · 4 comments
Labels
good first issue Good for newcomers help wanted Extra attention is needed

Comments

@mjschock
Copy link

mjschock commented Nov 5, 2019

Describe the bug
Camelize/decamelize asymmetry when numbers are involved.

To Reproduce
Steps to reproduce the behavior:

>>> import humps
>>> humps.camelize('item1_entry')
'item1Entry'
>>> humps.decamelize('item1Entry')
'item_1entry'
>>> humps.decamelize('item1entry')
'item_1entry'
>>> humps.camelize('N1_item')
'N1Item'
>>> humps.decamelize('N1Item')
'n1item'
>>> humps.decamelize('N1item')
'n1item'
>>> humps.camelize('n1_item')
'n1Item'
>>> humps.decamelize('n1Item')
'n_1item'

Expected behavior

>>> import humps
>>> humps.camelize('item1_entry')
'item1Entry'
>>> humps.decamelize('item1Entry')
'item1_entry'
>>> humps.decamelize('item1entry')
'item1entry'
>>> humps.camelize('N1_item')
'N1Item'
>>> humps.decamelize('N1Item')
'N1_item'
>>> humps.decamelize('N1item')
'N1item'
>>> humps.camelize('n1_item')
'n1Item'
>>> humps.decamelize('n1Item')
'n1_item'

Additional context
This is for version 1.3.1 of pyhumps.

@antonagestam
Copy link

@mjschock What behavior do you expect for humps.decamelize(humps.camelize('item_1entry'))?

@mjschock
Copy link
Author

mjschock commented Feb 2, 2020

@antonagestam hmm... i would expect the following:

>>> humps.camelize('item_1entry')
'item_1entry'
>>> humps.decamelize('item_1entry')
'item_1entry'

so

>>> humps.decamelize(humps.camelize('item_1entry'))
'item_1entry'

the general rules would be something like:

class IrreversibleProcessException(Exception):
    pass


def camelize(chars, root=True):
    camelized_chars = []

    for i, char in enumerate(chars):
        if char == '_':
            continue

        if i in (0, 1, len(chars) - 1):
            camelized_chars.append(char)

            continue

        if chars[i-1] == '_' and char.isalpha() and char.islower():
            camelized_chars.append(char.upper())
        elif chars[i-1] == '_':
            camelized_chars.append('_')
            camelized_chars.append(char)
        else:
            camelized_chars.append(char)

    camelized_chars_string = ''.join(camelized_chars)

    if root and decamelize(camelized_chars_string, False) != chars:
        raise IrreversibleProcessException

    return camelized_chars_string


def decamelize(chars, root=True):
    decamelized_chars = []

    for i, char in enumerate(chars):
        if i in (0, 1, len(chars) - 1):
            decamelized_chars.append(char)

            continue

        if chars[i+1] == '_' or chars[i-1] == '_':
            decamelized_chars.append(char)
        elif (chars[i-1].isalpha() and chars[i-1].islower()) and char.isupper():
            decamelized_chars.append('_')
            decamelized_chars.append(char.lower())
        elif not chars[i-1].isalpha() and char.isalpha() and char.isupper():
            decamelized_chars.append('_')
            decamelized_chars.append(char.lower())
        else:
            decamelized_chars.append(char)

    decamelized_chars_string = ''.join(decamelized_chars)

    if root and camelize(decamelized_chars_string, False) != chars:
        raise IrreversibleProcessException

    return decamelized_chars_string


assert camelize('item1_entry') == 'item1Entry'
assert camelize('N1_item') == 'N1Item'
assert camelize('n1_item') == 'n1Item'
assert camelize('item_1entry') == 'item_1entry'
assert camelize('ADVERTISED_1000baseT_Full') == 'ADVERTISED_1000baseT_Full'

assert decamelize('item1Entry') == 'item1_entry'
assert decamelize('item1entry') == 'item1entry'
assert decamelize('N1Item') == 'N1_item'
assert decamelize('N1item') == 'N1item'
assert decamelize('n1Item') == 'n1_item'
assert decamelize('item_1entry') == 'item_1entry'
assert decamelize('ADVERTISED_1000baseT_Full') == 'ADVERTISED_1000baseT_Full'

@nphilou nphilou added the good first issue Good for newcomers label Jun 28, 2020
@nficano nficano added the help wanted Extra attention is needed label Feb 19, 2021
@Hultner
Copy link

Hultner commented Jun 22, 2021

I think this is the same issue as #168, I've added these examples in that issue.

# Inconsistent behaviour
>>> humps.camelize("ab")
'ab'
>>> humps.camelize("a_b")
'a_b'
>>> humps.camelize("a_be")
'a_be'
>>> humps.camelize("a_best")
'a_best'
>>> humps.camelize("ar_best")
'arBest'
# Incorrect roundtrip
>>> humps.decamelize(humps.camelize("area_b"))
'areab'



# Correct
>>> humps.decamelize(humps.camelize("aa_bb"))
'aa_bb'
# Wrong
>>> humps.decamelize(humps.camelize("aa_b"))
'aab'
# Correct
>>> humps.decamelize(humps.camelize("a_b"))
'a_b'
>>> humps.decamelize(humps.camelize("a_bb"))
'a_bb'


# Frist is wrong, second correct
>>> humps.camelize("a_b")
'a_b'
>>> humps.camelize("aa_ab")
'aaAb'

@Hultner
Copy link

Hultner commented Jun 22, 2021

I'd recommend using property based testing as this is a very classic round trip case.
Here's an example test that catches these type of failures.

import humps.main
from hypothesis import given
from hypothesis import strategies as st


@given(str_or_iter=st.text(min_size=1))
def test_roundtrip_camelize_decamelize(str_or_iter):
    value0 = humps.main.camelize(str_or_iter=str_or_iter)
    value1 = humps.main.decamelize(str_or_iter=value0)
    assert str_or_iter == value1, (str_or_iter, value1)

Results:

===================================== FAILURES =====================================
________________________ test_roundtrip_camelize_decamelize ________________________
[gw0] darwin -- Python 3.9.0 ***

    @given(str_or_iter=st.text(min_size=1))
>   def test_roundtrip_camelize_decamelize(str_or_iter):

src/tests/schemas/test_query_params.py:28:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

str_or_iter = 'Aa'

    @given(str_or_iter=st.text(min_size=1))
    def test_roundtrip_camelize_decamelize(str_or_iter):
        value0 = humps.main.camelize(str_or_iter=str_or_iter)
        value1 = humps.main.decamelize(str_or_iter=value0)
>       assert str_or_iter == value1, (str_or_iter, value1)
E       AssertionError: ('Aa', 'aa')
E       assert 'Aa' == 'aa'
E         - aa
E         + Aa

src/tests/...py:31: AssertionError
------------------------------------ Hypothesis ------------------------------------
Falsifying example: test_roundtrip_camelize_decamelize(
    str_or_iter='Aa',
)

@mjschock mjschock closed this as completed Oct 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good first issue Good for newcomers help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

5 participants