Splitting a string (case sensitive)

Viewed 138

I have been facing an issue with splitting a string.

Here are the cases:

  1. test_str = "4years3months5days" -- output=4.3

  2. test_str = "3 months 2 days" -- output=0.3

  3. test_str = "4 years 2 months" -- output=4.2

  4. test_str = "4 Years 3 Months" -- output=4.3

  5. test_str = "4.6" -- output=4.6

  6. test_str = "4Y3M" -- output=4.3

(case sensitive here in few cases)

code:

test_string = "3months5days"

print("length=",len(test_string))
# printing original string  
print("The original string : " + test_string) 

if type(test_string) is bool:
    print(0)
elif len(test_string) == 1 or len(test_string) == 3:
    test_string = float(test_string)
    print("converted=",test_string)

else:
    temp = re.findall(r'\d+', test_string) 
    res = list(map(int, temp)) 
    print(res)
    if len(res)==1:
        print(float(res[0]))
    else:
        print(str(res[0])+'.'+str(res[1]))

I am able to write the code (or get from internet as well) for individual cases but not when combined. Any help?

4 Answers
import re
result = re.match(pattern, string)

You can create different patterns and use regular expression.

regex = r"(\d+)years(\d+)months(\d+)days"
match = re.search(regex, "4years3months5days")
print(match)
if match != None:
 print("Match at index % s, % s" % (match.start(), match.end()))
 print(match.group(0),match.group(1),match.group(2),match.group(3))

If you don't know about regular expression then follow the link for documentationRegular Expression

Basically, you need to find the substrings "years" and "months" and catch the substring that comes before (or between) them.

So here is a "naive" approach based on substring search and without regular expressions:

years = test_str.lower().find("years")
if years == -1:
    result = "0."
else:
    result = test_str[:years].strip() + "."

months = test_str.lower().find("months")
if months == -1:
    result += "0"
else:
    if years != -1:
        result += test_str[years+5:months].strip()
    else:
        result += test_str[:months].strip()

Here's a flexible version that passes all your test cases, using regular expressions. First, I'll define and compile* the regular expressions:

import re

# This pattern checks for the numeric version of your input: one or more
# digits, followed by a period, and then one or more digits.
numeric_pattern = re.compile(r'\d+\.\d+$')

# This one looks for two optional groups: each is one or more digits
# followed by 'y' or 'years', or 'm' or 'months'. The capturing groups
# are named, so we can tell which is which even if we only find one.
word_pattern = re.compile(
    # Written on two lines for clarity, but Python automatically
    # combines string literals inside parentheses:
    r'(?:(?P<years>\d+)y(?:ears)?)?'
    r'(?:(?P<months>\d+)m(?:onths)?)?'
)

Then I define a function to check these patterns against a supplied string:

def get_year_month(string):
    # Rather than deal with spaces and capitalization in our regexes,
    # we can normalize the input string first.
    string = string.lower().replace(' ', '') 
    
    # Check for the simpler case first. If it's a match, return as-is.
    if numeric_pattern.match(string):
        return string
    
    # Otherwise, check for words. (This pattern will ALWAYS match,
    # because each half is an optional group.)
    match = word_pattern.match(string)

    # Whatever it doesn't find is set to 0.
    years = match.group('years') if match.group('years') else 0
    months = match.group('months') if match.group('months') else 0
    return f'{years}.{months}'

Looping over a list of your inputs and expected outputs is a simple way to verify if it's working. It doesn't throw an error, so we know all the tests pass.

tests = [
    ('4years3months5days', '4.3'),
    ('3 months 2 days', '0.3'),
    ('4 years 2 months', '4.2'),
    ('4 Years 3 Months', '4.3'),
    ('4.6', '4.6'),
    ('4Y3M', '4.3'),
]

for string, result in tests:
    assert(get_year_month(string) == result)

*Even though Python caches regexes, I've found that, when your regexes will be reused multiple times, compiling them can still be dramatically faster, for some reason, even when the number of regexes isn't anywhere near maxing out the cache limit.

Regardless of performance, defining your regexes all in one place and giving them clear names can often make your code clearer and more readable.

Use regex:

import regex

a_list = [
    '4years3months5days',
    '3 months 2 days',
    '4 years 2 months',
    '4 Years 3 Months',
    '4.6',
    '4Y3M',
    '10Y 3M',
    '10Y3M'
]

t = [
    '(?:(?<!years(?:\s*?)))(\d+?)(?:\s*?)(?:(?=months))',
    '(\d+?)(?:\s*?)(?:years)(?:\s*?)(\d+?)(?:(?=(?:\s*?)months))',
    '\\b(\d+?)(?:\.)(\d+?)\\b',
    '(\d+?)(?:\s*?)Y(?:\s*?)(\d+?)(?:(?=m))'
]

longest_len = len(max(t))

for i in a_list:
    for j in t:
        if regex.match(fr'{j}', i, flags=regex.I):
            r = regex.match(fr'{j}', i, flags=regex.I).groups()
            
            if len(r) > 1:
                print(f'{i:{longest_len}}', '=>', '.'.join(r))
            else:
                print(f'{i:{longest_len}}', ' => ', '0.', *r, sep='')
4years3months5days     => 4.3
3 months 2 days        => 0.3
4 years 2 months       => 4.2
4 Years 3 Months       => 4.3
4.6                    => 4.6
4Y3M                   => 4.3
10Y 3M                 => 10.3
10Y3M                  => 10.3
Related