Calculate working cut-off date

Asked

Viewed 388 times

1

I have a project in which the user gives a day and a month where he would start some task and the program should, based on some rules, return a final date.

  • Example of rules: maximum running days = 90; exclude weekends and holidays

This is my current code, I believe I’m quite lost, I even tried to use a date library, but without success.

import json
import requests
from datetime import date

l_d = 90
l_h = 1000
n = 0
lista = []

p = int(input("Nº de Pessoas: "))
y = int(input("Ano: "))
d = int(input("Dia de Inicio: "))
m = int(input("Mês de Inicio: "))

y_str = str(y)

api1 = "https://api.calendario.com.br/?json=true&token=ZmVsaXBlLmEubWF6aWVyaUBob3RtYWlsLmNvbSZoYXNoPTczMDA3OTc0&ano="
api2 = "&estado=SP&cidade=Sao_Paulo"
url = api1 + y_str + api2

json_data = requests.get(url).json()

for x in json_data:
    feriados = json_data[n]["date"]
    n+=1
    novalista = lista.append(feriados)

#dm = "{}/{}/{}".format(d, m, y)

print("")
# print(lista[n-1]) # colocar [ e alguem nº aqui, da pra exibir a data correspondente a posição dentro da lista, começando pelo nº ZERO (0) que é a 1ª posição dentro da lista]
# while n > 0:
    # print("É feriado em SP no dia",lista[n-1])
    # n = n -1

data = ("{}/{}/{}").format(d, m, y)
print(data)

2 answers

1

THE JSON1 returned by API is an array (as it is bounded by []), so he’s converted to a list (that is, its variable json_data is a list).

And the way to go through the elements of a list is simply to use for elemento in lista:. Your variable n is not necessary here, because the x of his first for will already be the array element at each iteration.

Another detail is that the method append nay returns another list, then assign your return to another variable is not required. Just call append in the list you want to add the element and ready:

feriados = []
for x in json_data:
    feriado = x["date"]
    feriados.append(feriado)

Thus, you will have a list of the holidays. Note that on loop I changed the variable name feriados (plural) by feriado (singular), because it represents only a single holiday. The variable lista had a too generic name (is it a list of what?), and I changed her name to feriados (yes in the plural), because then it becomes clearer what it contains. It may seem like a stupid detail, but give better names for variables, functions, modules, etc., helps a lot when programming (I just kept the x of its original code for you to realize that you could use it instead of ignoring it and using the n, but he might have another name too, like elemento or dados_feriado, or something like that).

Obs: actually this variable feriado is not so necessary, could do directly feriados.append(x['date']).


Another alternative to the loop above is to use the syntax of comprehensilist on, much more succinct and pythonic. The line below is equivalent to for above, producing the same list containing the JSON dates:

feriados = [ dados_feriado['date'] for dados_feriado in json_data ]

There is still another alternative, which is to use the function map:

feriados = list(map(lambda dados_feriado: dados_feriado['date'], json_data))

The first parameter passed to map is a lambda: a function that will be applied to each element in the list. In this case, it returns the key value date, and does this for each element in the list. How map returns a iterator, i use list to convert it to a list.


Check if dates are in the list

Here is an important detail: as I have already said here, here and here, dates have no format.

A date is just a concept, an idea: it represents a specific point in the calendar.

The date of "January 2, 1970", for example, represents this: the specific point of the calendar that corresponds to the 2nd of January of 1970. To express this idea in text form, I can write it in different ways:

  • 02/01/1970 (a common format in many countries, including Brazil)
  • 1/2/1970 (American format, reversing day and month)
  • 1970-01-02 (the format ISO 8601)
  • Two of January 1970 (in good Portuguese)
  • January 2nd, 1970 (in English)
  • 1970 年 1 月 2 日 (in Japanese)
  • and many others...

Note that each of the above formats is different, but all represent the same date (the same numerical values of the day, month and year).

That being said, a JSON does not define a specific type for dates, so the previously created list does not actually have dates: it has strings, which represent dates in a specific format (in this case "dd/mm/yyyy"). Knowing this, we can proceed.


In Python, you can use module datetime to work with dates. As in this case you are only interested in the day, month and year, and do not care about the time, you can use the class date. Example:

ano = int(input("Ano: "))
mes = int(input("Mêsde Inicio: "))
dia = int(input("Dia de Inicio: "))
dt = date(ano, mes, dia)

Thus, the variable dt shall be the date corresponding to the day, month and year informed.

Then just make a loop adding one day to this date (using a timedelta for this), and checking whether the date is weekend or holiday (and if it is not, you consider that day in your count).

To check if it is a weekend, just use the method weekday, that returns values between zero (Monday) and 6 (Sunday). That is, if the value is between 0 and 4, it is not weekend.

Already to check if the date is a holiday, it is not enough to check whether dt is on the list feriados. Remember that dates have no format, and dt is a date (that is, an object representing the concept of a date, with no specific format), but feriados is a list of strings (where these strings represent dates in a specific format). That is, to check if a date is in the list, we need to format it to the same format as the strings are in. For this, we use the method strftime.

Then the code would look like this:

from datetime import date, timedelta

ano = int(input("Ano: "))
mes = int(input("Mês de Inicio: "))
dia = int(input("Dia de Inicio: "))
dt = date(ano, mes, dia)

contagem_dias = 0
while contagem_dias < 90:
    dt += timedelta(days=1)
    if dt.weekday() <= 4 and dt.strftime('%d/%m/%Y') not in feriados:
        contagem_dias += 1

print('data final:', dt)

The while adds up a day on the date, and only increases the counter if it is not weekend and if it’s not a holiday.


And if I change the year?

The above solution is still somewhat "naive", as it does not consider the case in which year it changes. For example, if you start with a date in the last months of the year, by counting 90 days the final result will be the following year, but you only search for the holidays of the current year. So I suggest first creating a function that seeks the holidays of a given year:

def buscar_feriados(ano):
    # faz o request para a API, passando o ano
    api1 = "https://api.calendario.com.br/?json=true&token=ZmVsaXBlLmEubWF6aWVyaUBob3RtYWlsLmNvbSZoYXNoPTczMDA3OTc0&ano="
    api2 = "&estado=SP&cidade=Sao_Paulo"
    url = api1 + str(ano) + api2

    json_data = requests.get(url).json()
    return [ dados_feriado['date'] for dados_feriado in json_data ]

And then you use this function in your code. First you upload the holidays of the current year, and on while you check if the year has changed (and if you have changed, update the list with this year’s holidays):

dt = date(ano, mes, dia)
feriados = buscar_feriados(ano)

contagem_dias = 0
ano_atual = ano
while contagem_dias < 90:
    dt += timedelta(days=1)

    if dt.year != ano_atual: # mudou o ano
        feriados.extend(buscar_feriados(dt.year))
        ano_atual = dt.year

    if dt.weekday() <= 4 and dt.strftime('%d/%m/%Y') not in feriados:
        contagem_dias += 1

print('data final:', dt)

The method extend adds the elements of the list returned by buscar_feriados on the list feriados. With this, the list of holidays is updated as the year changes.

Finally, I suggest reviewing this holiday API you are using, as it returns things like "April First" and "Valentine’s Day" (June 12th), which although they are "special" dates, are not necessarily holidays (not in the sense that people who would normally work on this day will not need to work). But then I believe that this treatment is already beyond the scope of the question, because "holidays" is a more complex subject than it seems: you have to decide if you will consider national, state and municipal holidays, there are specific "holidays" depending on the context (insurers do not usually work on security day, the judiciary has its own calendar, etc.), there are holiday "amendments" (if it falls on Thursday, do you consider the sixth as a working day or also skip it on the count? - the above code does not "amend" any holiday), etc.


Another alternative would be to create the list of holidays containing instances of date, instead of strings. In this case, strings would have to be converted to date, using the method datetime.strptime:

from datetime import date, timedelta, datetime

# agora a lista contém instâncias de date em vez de strings
feriados = [ datetime.strptime(dados_feriado['date'], '%d/%m/%Y').date() for dados_feriado in json_data ]

contagem_dias = 0
dt = date(ano, mes, dia)
while contagem_dias < 90:
    dt += timedelta(days=1)
    if dt.weekday() <= 4 and dt not in feriados:
        contagem_dias += 1

print('data final:', dt)

In the case, datetime.strptime returns a datetime, then I use the method date() to convert it to an object date. Thus, the list feriados no longer contains strings, but instances of date. Therefore, in the while I no longer need to convert the dates to string, and can directly check whether the list feriados contains the date dt.


1: see more about JSON syntax here, here, here and here.

0

This API only passes the holidays anyway?
I think I could use this same API to know if it is weekend or not. But anyway.....

Using the datetime library, you should first format the "date" information that was extracted from the json for the same datetime format (YYYY-MM-DD), as it will make our life easier...
Now to check what you need:

from datetime import datetime, timedelta
ano = input()
mes = input()
dia = input()
aux = 0
dt = datetime(ano, mes, dia)
while(aux < 90):
   if dt.weekday() > 4 or dt in feriado_lista:
      pass
   else:
      aux += 1
   dt_fim += timedelta(days=1) 

print(datetime(ano, mes, dia))
print(dt)

Notes
- datetime.datetime.weekday(): If return value greater than 4 is weekend.
- datetime.timedelta(): Adds a missing value of the datetime object.

  • Unfortunately the API only literally passes the holidays and found nothing + complete :/

Browser other questions tagged

You are not signed in. Login or sign up in order to post.