#๐Ÿ”’ CS50 [working.py] regex value question

145 messages ยท Page 1 of 1 (latest)

slate wraith
#

Hello again everyone, I'm currently working on the CS50 Python course's working.py assignment, and while I've figured it out for the most part, I have a silly problem I would like some advice on.

First, here is my code:

import re

def main():
    print(convert(input("Hours: ")))

def convert(s):
    if time := re.search(r"(\d{1,2}):?(\d{2})? (AM|PM) to (\d{1,2}):?(\d{2})? (AM|PM)", s, re.IGNORECASE):
        h1, h2 = (int(time.group(1)), int(time.group(4)))
        m1, m2 = (int(time.group(2)), int(time.group(5)))
        p1, p2 = (time.group(3), time.group(6))
        if any((h1, h2)) > 12 and any((m1, m2)) >= 60:
            raise ValueError
        if h1 < 12 and p1 == "PM":
            h1 += 12
        if h2 < 12 and p2 == "PM":
            h2 += 12
        if h1 == 12 and p1 == "AM":
            h1 -= 12
        if h2 == 12 and p2 == "AM":
            h2 -= 12
        if m1 == None:
            m1 = ":00"
        if m2 == None:
            m2 = ":00"
        return f"{h1:02d}:{m1:02d} to {h2:02d}:{m2:02d}"
    else:
        raise ValueError

if __name__ == "__main__":
    main()

The assignment calls for a time prompt in the 12-hour format, for example: 9 AM to 5 PM, 9:30 AM to 3:30 PM etc and the output is on the 24-hour format, like 09:00 to 17:00. The problem I'm experiencing is that when the minutes are declared as time.group(2) and time.group(5) respectively but omitted from the prompt, the AM|PM value takes the minute's group position and results in disarray. What would be the best and most efficient way to approach this? And also, if you have any other advice on how to shorten / optimize my code, any advice would be really appreciated as I am still learning, thank you!

dire currentBOT
#

@slate wraith

Python help channel opened

Remember to:

  • Ask your Python question, not if you can ask or if there's an expert who can help.
  • Show a code sample as text (rather than a screenshot) and the error message, if you've got one.
  • Explain what you expect to happen and what actually happens.

:warning: Do not pip install anything that isn't related to your question, especially if asked to over DMs.

#

Hey @slate wraith!

It looks like you pasted Python code without syntax highlighting.

Please use syntax highlighting to improve the legibility of your code and make it easier for us to help you.

To do this, use the following method:
```py
print('Hello, world!')
```

This will result in the following:

print('Hello, world!')```
You can **edit your original message** to correct your code block.
hushed grail
#

will they mix and match formats?

slate wraith
#

They should be separated by space and the user could also choose the opposite, like 5:30 PM to 1 AM

#

The assignment also doesn't allow format mixing, other than omitting or inputting the minutes in the prompt

#

So it should be something like h(:m) (AM|PM) to h(:m) (AM|PM) sort of thing

hushed grail
#

Can you give an example prompt that gives the problem result?

slate wraith
#

Yes, of course, let me produce an error really fast and will copy paste here

#

So, writing for example 11 AM to 5 PM produces the following error:

#
Hours: 11 AM to 5 PM
Traceback (most recent call last):
  File "/mnt/external/Dropbox/programming/builder/CS50/tests/working3.py", line 30, in <module>
    main()
  File "/mnt/external/Dropbox/programming/builder/CS50/tests/working3.py", line 4, in main
    print(convert(input("Hours: ")))
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/mnt/external/Dropbox/programming/builder/CS50/tests/working3.py", line 9, in convert
    m1, m2 = (int(time.group(2)), int(time.group(5)))
              ^^^^^^^^^^^^^^^^^^
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'

hushed grail
#

you could do int(time.group(2) or 0)

slate wraith
#

This is because there are no minutes written in my prompt, hence the program thinking that the AM|PM thing is time.group(2) instead

hushed grail
#

your groups shouldn't shuffle around based on the prompt though

slate wraith
#

That's what I also thought! That's what baffles me

hushed grail
#
convert("9:30 AM to 3:30 PM")
convert("11 AM to 5 PM")
#

I ran these 2 tests

#
('9', '30', 'AM', '3', '30', 'PM')
('11', None, 'AM', '5', None, 'PM')
#

these are the resulting groups

hushed grail
#

if no minutes, then minutes are 0

slate wraith
#

Right, that's why I also have this in my code:

if m1 == None:
      m1 = ":00"
if m2 == None:
      m2 = ":00"
dire currentBOT
#

Hey @slate wraith!

It looks like you pasted Python code without syntax highlighting.

Please use syntax highlighting to improve the legibility of your code and make it easier for us to help you.

To do this, use the following method:
```py
print('Hello, world!')
```

This will result in the following:

print('Hello, world!')```
You can **edit your original message** to correct your code block.
hushed grail
#

this lets you catch it a bit earlier and a bit more elegantly

#

!e

number1 = '10'
number2 = None

print(int(number1 or 0))
print(int(number2 or 0))


dire currentBOT
slate wraith
#

Hmmmm

#

I am a bit confused

#

I know the minutes return None if they are non-existent in the prompt

#

But I thought writing the two if statements above fixes this

hushed grail
#

it's because you're trying to convert None to int

slate wraith
#

Ohhhhhh

hushed grail
#

then you can completely get rid of the if m1 == None

slate wraith
#

OH!!!

#

I think I get it!

hushed grail
#
m1, m2 = (int(time.group(2) or 0), int(time.group(5) or 0))
slate wraith
#

Ohhhhhhhhh

hushed grail
#

do you understand how or works here?

slate wraith
#

Yes yes of course

#

I just didn't know we could have an or 0 scenario inside a tuple

hushed grail
#

you can or any expression

#

it simply takes the first "truthy" result or the final result

#

so if none of the expressions are truthy, it will fall on the last one

#

!e

print(0 or [] or None)
print(None or 0 or [])
print(None or 5 or [])
dire currentBOT
hushed grail
#

really useful for converting non-truthy data into a format you prefer

slate wraith
#

Oh wow, that is truly eye-opening

#

I had no idea Python was that flexible

hushed grail
#

I work with a library that unfortunately has a lot of functions that return either a list, or None if no data is found

#

so I often do

children = list_relatives(node) or []
#

this way I'm forced into an empty list instead of None

slate wraith
#

Sometimes I also go by spoken logic and fall into human errors, but in reality, Python can understand it all

slate wraith
hushed grail
#

I used to do this before I learned the "or" trick

#
children = list_relatives(node)
if children:
    for child in children:
        ...
#
children = list_relatives(node) or []
for child in children:
    ...
#

now I can completely cut out the if

slate wraith
#

Oh yeah I see

hushed grail
#

if the list is empty, the loop will simply be skipped

slate wraith
#

Sorry, can I ask one more question?

hushed grail
#

Sure, go ahead, it's your thread ๐Ÿ™‚

slate wraith
#

The program works perfectly now thanks to your advice, so I also deleted the two if statements, but are there any other refinements I can do to optimize the code without jeopardizing its legibility?

#

I am trying to avoid arbitrary steps as much as possible

#

This is my code now:

#
import re

def main():
    print(convert(input("Hours: ")))

def convert(s):
    if time := re.search(r"(\d{1,2}):?(\d{2})? (AM|PM) to (\d{1,2}):?(\d{2})? (AM|PM)", s, re.IGNORECASE):
        h1, h2 = (int(time.group(1)), int(time.group(4)))
        m1, m2 = (int(time.group(2) or 0), int(time.group(5) or 0))
        p1, p2 = (time.group(3), time.group(6))
        if any((h1, h2)) > 12 and any((m1, m2)) >= 60:
            raise ValueError
        if h1 < 12 and p1 == "PM":
            h1 += 12
        if h2 < 12 and p2 == "PM":
            h2 += 12
        if h1 == 12 and p1 == "AM":
            h1 -= 12
        if h2 == 12 and p2 == "AM":
            h2 -= 12
        return f"{h1:02d}:{m1:02d} to {h2:02d}:{m2:02d}"
    else:
        raise ValueError

if __name__ == "__main__":
    main()
hushed grail
#

I think it looks good as it is! For future optimizations, I'd consider using the datetime module to parse the time

slate wraith
#

Oh okay, this is something I still haven't learned, but noted!

#

I feel there's a lot of redundantness in this part in specific:

if h1 < 12 and p1 == "PM":
            h1 += 12
        if h2 < 12 and p2 == "PM":
            h2 += 12
        if h1 == 12 and p1 == "AM":
            h1 -= 12
        if h2 == 12 and p2 == "AM":
            h2 -= 12
#

But it doesn't seem like there's much I can do about it, right?

hushed grail
#

a lot of "redundancies" could be offloaded to a separate function

slate wraith
#

Oh, right

hushed grail
#

so you could write another function like "convert_24hr_time"

#

and then handle it all there

#

it's the same logic, just handled elsewhere

#

but it keeps things neater

slate wraith
#

But it would technically result in the same amount of lines, right?

hushed grail
#

your convert function would be smaller, but you'd have a new function so it would technically be more lines overall

slate wraith
#

Right, I understand

hushed grail
#

but then you wouldn't need an if for your times

#

you'd just say

h1 = convert_24hr_time(h1, p1)
h2 = convert_24hr_time(h2, p2)
slate wraith
#

Ahhh right, yes

hushed grail
#

learning how to split code up into smaller functions becomes very important

slate wraith
#

I am still not used to writing separate functions for separate purposes, but I probably should get used to it, as programs become more complex

#

Yes, exactly

hushed grail
#

you're always better off having 3 separate smaller functions rather than 1 mega function that does 3 things

#

then you just have 1 function call those 3 functions

#

it makes things more modular and reusable

#

it's ok if you don't write it that way all on the first go though

#

this is where refactoring becomes important

slate wraith
#

I think I will try to rewrite this program now with a new convert24h function, just to practice common Python nomenclature

hushed grail
#

and yeah, datetime would be good practice too

slate wraith
#

I would also like to eventually work with other people in software, so I want to refine my Python skills and its legibility as much as possible

#

For now, thank you so much for all your help!

hushed grail
#

np!

#

I can show a datetime example if you like

slate wraith
#

I learned a ton today, thank you so much

#

Oh, if that's no trouble for you, yes please

hushed grail
#

('9', '30', 'AM', '3', '30', 'PM')

#

I'm just going to use this as the initial example

#

because you already have this data easily accessible

#

the first 3 indices will be the 1st time and the next 3 will be the 2nd time

slate wraith
#

Ohhh

#

Is this a builtin Python function?

hushed grail
#

you need to import it, but yeah, it's part of the standard library

slate wraith
#

Or a library?

#

Ohhh okay

hushed grail
#

it can be a bit confusing at first because the module and class name are the same

#

so from datetime import datetime is commonly how you import it

slate wraith
#

In order to avoid that whole datetime.datetime() type of thing, I'd imagine

hushed grail
#

exactly

slate wraith
#

Yeah

#

Wow, Python has libraries for everything nowadays

hushed grail
#

so with datetime, there's a formatting syntax that lets us convert a string into a "datetime object"

slate wraith
#

But it feels good to have figured this out the regex way

hushed grail
#

but we need to tell it exactly how our string looks

hushed grail
#

I would still use regex to parse the text

#

but then you can feed that information into the datetime string and let it figure out the conversion

#

!e

from datetime import datetime

times = ('9', '30', 'AM', '3', '30', 'PM')
t1, t2 = times[:3], times[3:]

print(t1)
print(t2)
dire currentBOT
hushed grail
#

so I start by splitting up the 2 times, but unfortunately datetime doesn't like digits that aren't padded

#

it wants 09, not 9

#

we can do this in a super lazy way using a list comp or the map function

slate wraith
#

Yeah, cause the assignment still called for a py f"{datetime[0]}:{datetime[1]} to {datetime[3]}:{datetime[4]}" type of thing

#

This is probably wrong, but

hushed grail
#

!e

from datetime import datetime

times = ('9', '30', 'AM', '3', '30', 'PM')
t1, t2 = times[:3], times[3:]

print([f'{i:>02}' for i in t1])
print([f'{i:>02}' for i in t2])
dire currentBOT
hushed grail
#

so this solves the zero-padding issue

#

but now we also need a string

slate wraith
#

Ohhhhh

hushed grail
#

we can use str.join to make a string from this

#

!e

from datetime import datetime

times = ('9', '30', 'AM', '3', '30', 'PM')
t1, t2 = times[:3], times[3:]

print('-'.join([f'{i:>02}' for i in t1]))
print('-'.join([f'{i:>02}' for i in t2]))
dire currentBOT
hushed grail
#

this is good enough for datetime to start working with

slate wraith
#

Wow, this is so great, this could come in very handy in the future

#

Thank you so much for all your help today

#

I genuinely appreciate it you taking the time to help me out

hushed grail
#

np!

slate wraith
#

!close

dire currentBOT
#
Python help channel closed

This help channel has been closed and it's no longer possible to send messages here. If your question wasn't answered, feel free to create a new post in #1035199133436354600. To maximize your chances of getting a response, check out this guide on asking good questions.