How can I implement multiple units simultaneously into a reminder command in d.py?

Question:

Short story long, here’s what I’m trying to do; I am in the process of making a reminder command for my discord bot, and I need it to take in multiple arguments, each containing a keyword (in this case a letter) that defines which unit I am working with (hours/minutes/seconds) so I can convert/keep as is accordingly, and then use await sleep(duration in seconds) followed by await ctx.send(f'{ctx.author.mention} timer is up')

Lastly, I was told using asyncio puts too much strain on the bot for a long duration, and that I should use a database, I’m still not sure how that works, however I’ve read some stuff about communicating with MySQL through python code and it doesn’t look to hard; I’m just not sure how a database would be helpful. (I’ve never used a database before and haven’t been coding for long)

PS: I’m asking for concepts and sources of information regarding methods I could use to implement, I do not appreciate anyone writing the whole thing for me; I would rather write it myself and fully understand my code, as well as learn new things.

Currently it can only take in a single unit, and then convert it into seconds etc. I could set it to take in multiple arguments, but then I would be forced to provide all three at once (hours/minutes/seconds)

@commands.command(name="reminder")
    async def reminder(self, ctx: commands.Context, time: str, msg="no reminder name was provided."):
        time_period = time

        if time_period.endswith("h"):
            time_period = time_period[:-1]
            time_period = int(time_period)
            await s(time_period * 3600)
            await ctx.send(f'{ctx.author.mention} your timer is done') 


        if time_period.endswith("s"):
            time_period = time_period[:-1]
            time_period = int(time_period)
            await s(time_period)
            await ctx.send(f'{ctx.author.mention} your timer is done') 
   
        if time_period.endswith("m"):
            time_period = time_period[:-1]
            time_period = int(time_period)
            await s(time_period * 60)
            await ctx.send(f'{ctx.author.mention} your timer is done')

In Extension to the Original Question (check the comments):

import re 
import discord
from discord.ext import commands
from asyncio import sleep as s

class beta(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot




def convert(time_str: str) -> int:
    time_dict = {'s': 1, 'm': 60, 'h': 3600, 'd': 3600*24}
    # this pattern looks for days, hours, minutes, seconds in the string, etc
    regex_pattern = r"^(?P<day>d+d)?(?P<hour>d+h)?(?P<minute>d+m)?(?P<second>d+s)?"
    matches = re.match(regex_pattern, time_str)
    total_time = 0
    for group in matches.groups():
        if not group:
            continue

        unit = group[-1]
        val = int(group[:-1])
        amount = val * time_dict[unit]
        total_time += amount
    return total_time


@commands.command(name="reminder")
async def reminder(self, ctx: commands.Context, time: str, msg: str = None):
    await ctx.send(f"Reminder has been set for {time}")
    if msg is None:
        msg = "no reminder name was provided."

    try:
        time_period = convert(time)
    except Exception as e:
        await ctx.send(f"Invalid time string")
        return

    await s(time_period)
    await ctx.send(f'{ctx.author.mention} your timer is done')


def setup(bot: commands.Bot):
    bot.add_cog(beta(bot))

This the code I used, and for the record it doesn’t even get to await ctx.send(f'Reminder set for' {time})

Errors: ValueError: invalid literal for int() with base 10: '5s'

and: raise BadArgument('Converting to "{}" failed for parameter {}".'.format(name, param.name)) from exc discord.ext.commands.errors.BadArgument: Converting to "int" failed for parameter "time".

Asked By: Had

||

Answers:

This is a better explanation of this answer but adapted for this use case.

The answer uses regular expressions (aka `regex) for parsing the string. realpython has some articles on regex here.

It expects strings that are all one word with lowercase letters for seconds (s), minutes (m), hours (h), and days (d). With the letters separated by the values alone. Valid examples:

  • 60s (60 seconds)
  • 120s (120 seconds)
  • 2m (2 minutes)
  • 2m30s (2 minutes and 30 seconds)
  • 1h (one hour)
  • 12h30m60s (twelve hours, thiry minutes, and sixty seconds)
  • etc

Hopefully that makes sense.

# with your imports
import re

@commands.command(name="reminder")
async def reminder(self, ctx: commands.Context, time: str, msg: str = None):
    if msg is None:
        msg = "no reminder name was provided."

    # define a mapping of units to number of seconds per unit
    time_dict = {'s': 1, 'm': 60, 'h': 3600, 'd': 3600*24}

    # this pattern looks for days, hours, minutes, seconds in the string, etc
    regex_pattern = r"^(?P<day>d+d)?(?P<hour>d+h)?(?P<minute>d+m)?(?P<second>d+s)?"
    # find any matches using the pattern
    matches = re.match(regex_pattern, time)

    # define the amount of seconds as 0 initially
    total_time = 0

    # loop over the matches
    for group in matches.groups():
        if not group:
            continue

        # grab the unit (ie either; s, m, h, d
        unit = group[-1]

        # grab the value
        val = int(group[:-1])

        # times the value by seconds per unit
        amount = val * time_dict[unit]

        # add the time to the total amount of time
        total_time += amount

    await s(total_time)
    await ctx.send(f'{ctx.author.mention} your timer is done')

What the other example does; is separate out this conversion logic into a separate function. Applying that to your example:

# with your imports
import re

def convert(time_str: str) -> int:
    time_dict = {'s': 1, 'm': 60, 'h': 3600, 'd': 3600*24}
    # this pattern looks for days, hours, minutes, seconds in the string, etc
    regex_pattern = r"^(?P<day>d+d)?(?P<hour>d+h)?(?P<minute>d+m)?(?P<second>d+s)?"
    matches = re.match(regex_pattern, time_str)
    total_time = 0
    for group in matches.groups():
        if not group:
            continue

        unit = group[-1]
        val = int(group[:-1])
        amount = val * time_dict[unit]
        total_time += amount
    return total_time


@commands.command(name="reminder")
async def reminder(self, ctx: commands.Context, time: str, msg: str = None):
    if msg is None:
        msg = "no reminder name was provided."

    try:
        time_period = convert(time)
    except Exception as e:
        await ctx.send(f"Invalid time string")
        return

    await s(time_period)
    await ctx.send(f'{ctx.author.mention} your timer is done')

This allows you to wrap the convert function in a try/except in case the user provides a string that isn’t valid.

Answers to comment questions below

Why if not group: continue?

The regex pattern we’ve defined is looking for parts of the string that matches for days, hours, minutes, and seconds. We’re doing this via groups so we’re trying to split the string in 4 distinct groups for each part. If it doesn’t find something that matches for one of those,it still creates an empty group.

Example:

import re
regex_pattern = r"^(?P<day>d+d)?(?P<hour>d+h)?(?P<minute>d+m)?(?P<second>d+s)?"
match = re.match(regex_pattern, "1h5m")
print(match.groups())

This prints:

(None, '1h', '5m', None)

As you can see, we have None for days and seconds as nothing was found to match those. Therefore, we want to skip the groups that are empty. Hence the if not group: continue. (continue in a for loop just moves on to the next item in the list/iterable).

This is also why we don’t need to specify particular combinations that can be used; we’re looking for everything at once in the provided string. If something is missing; that’s fine we just skip it. We’re also counting the ‘total_seconds’ as we go; converting values of another unit to seconds and adding to it.

Why use [-1] and [:-1]?

group in our loop is just a string. As we can see from our earlier example; we’ll have two valid strings: 1h and 5m. We can using indexing ([] – just like we can do one lists) to access different characters in the string.

[-1] will get the last character in the string (and the last item in a list) so doing group[-1] will get us the unit – as it’s the last bit of the string.

[:-1] is an indexing operation that will return everything in the string; up to the last character. So it basically just omits the unit we just found. You can do this in other ways; like val = int(group.replace(unit, "")) but this was relatively clean.

You can read more about string indexing here.

Answered By: ESloman
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.