
timeboard
je Python biblioteka koja kreira rasporede radnih razdoblja i izvodi proračune kalendara preko njih. Možete izraditi standardne kalendare radnih dana, kao i niz drugih rasporeda, jednostavnih ili složenih.
Dokumentaciju možete pronaći ovdje.
Ovdje provjerite GitHub repo.
Pronađite ga na PyPI ovdje.
Priča
Počelo je sa slučajem broja stanovnika. Naša je tvrtka uvela KPI-je koji uključuju prihod po zaposleniku, pa smo morali znati prosječni godišnji broj zaposlenih u svakom timu. Već sam pisao Python skripte, pa nisam bio zastrašen.
Da bih dobio broj zaposlenih, morao sam izračunati broj radnih dana koje je svaki zaposlenik proveo s tvrtkom u godini. Pande će to riješiti za sekundu, pomislila sam. Ali pokazalo se da Pandas nije mogao.
Ruski poslovni kalendar je glomazan. Zamijenjuju radne dane sa subotom ili nedjeljom kako bi popunili praznine između praznika i vikenda. Primjerice, na posao morate doći u subotu u veljači da bi vam se nadoknadio slobodan ponedjeljak koji prethodi prazničnom utorku negdje u svibnju.
Shema za svaku godinu jedinstvena je. Pandasov kalendar radnih dana podržava samo jednosmjerne izmjene i dopune za promatranje praznika. Dakle, mogao bih pretvoriti radni dan u slobodan dan, ali ne i obrnuto.
Tada su u call centru bili operatori, a moja tjeskoba zamahnula je u drugu stranu. Rade u smjenama različite duljine, a jedna smjena nakon koje slijede tri smjene. Da bih dobio statistiku call centra, nije mi trebao kalendar radnog dana. Ipak, morao sam izbrojiti broj smjena određenog operatera u određenom vremenskom razdoblju.
I na kraju, neobičan problem. U mojoj lokalnoj prodavaonici automobila Honda, mehaničari rade po alternativnom tjednom rasporedu: ponedjeljak, utorak, subota i nedjelja ovog tjedna, te srijeda do petka sljedeći tjedan. Želio sam da me uvijek poslužuje određeni mehaničar, jer je onaj drugi jednom pokvario kočnice. Želio sam jednostavan način odrediti sljedeću smjenu "mog" mehaničara.
Ovi slučajevi imaju zajedničke temelje. Njihova rješenja oslanjala bi se na raspored vremenskih razdoblja "dežurstva" i "neradnika". Morali bismo biti u stanju sastaviti različito strukturirane rasporede prikladne za različite poslovne slučajeve. Upiti i izračuni koji prelaze raspored moraju razlikovati razdoblja "dežurstva" i "neažurnosti".
Nisam uspio pronaći Python paket koji je pružao sredstva za izgradnju i ispitivanje takvih rasporeda. Kako se dogodilo, imao sam malo slobodnog vremena da to sam napišem.

Koncept
timeboard
je Python biblioteka koja kreira rasporede radnih razdoblja i izvodi proračune kalendara preko njih. Ti se objekti nazivaju vremenskim pločama.
Tri su glavna koraka u razmišljanju o vremenskom rasporedu.
Počinjete s vremenskim intervalom koji postavlja granice vašeg kalendara. Sve će biti ograničeno na ovaj interval. Zove se (referentni) okvir. Okvir se sastoji od osnovnih jedinica. Osnovna jedinica je najmanje vremensko razdoblje potrebno za mjerenje kalendara. Na primjer, ako razmišljate u smislu radnih dana, tada je osnovna jedinica dan. Ako izgradite raspored višesatnih smjena, osnovna jedinica je jedan sat.

Na sljedećem koraku definirate pravila označavanja okvira u radne pomake. Radne smjene su vremenska razdoblja do kojih vam je stalo. Oni čine vaš kalendar. To su radne smjene koje želite rasporediti ili prebrojati. U standardnom kalendaru radnih dana radni pomak je dan (a bazna jedinica je i dan, pa se podudaraju).
U call centru radna smjena je razdoblje od nekoliko sati kada je određena smjena operatera na dužnosti. Osnovna jedinica vjerojatno će biti jedan sat, a svako radno mjesto sadrži (vjerojatno različit) broj baznih jedinica.
Slijed radnih pomaka koji ispunjavaju okvir naziva se vremenska crta.
Napokon, izradite jedan ili više rasporeda. Raspored je poput matrice postavljene preko vremenske crte. Njegova je svrha razlikovati dežurne radne smjene od onih izvan dužnosti.
S rasporedom treba nešto raditi kako bi se prijavilo radno mjesto na dužnosti ili izvan njega. Zbog toga dajete oznaku za svaki radni pomak, odnosno pravilo za njihovo označavanje dok je okvir označen na vremenskoj traci. Svaki raspored definira funkciju selektora koja pregledava oznaku radne smjene i vraća True za dežurne radne smjene i False u suprotnom. Ako ga ne poništite, vremensku crtu prati zadani raspored čiji birač vraća logičku vrijednost oznake.
Ponekad želite definirati nekoliko rasporeda za isti vremenski slijed. Na primjer, u pozivnom centru bit će raspored za pozivni centar u cjelini i zaseban raspored za svaki tim operatora. Isti radni pogon može se naći na dužnosti prema nekim rasporedima, a van službe prema ostalim.

Timeboard = vremenska linija + rasporedi. Točnije, vremenski raspored je skup radnih rasporeda koji se temelje na određenom vremenskom slijedu radnih smjena izgrađenim na referentnom okviru .
Nakon što nabavite vremenski raspored, možete obaviti koristan posao: napravite proračune kalendara kako biste riješili probleme poput onih opisanih u prologu.
Svako računanje izvedeno s vremenskim rasporedom je svjesno dužnosti. Pozvana metoda "vidi" samo radne smjene s navedenom dužnošću i zanemaruje ostale. Da bi se otkrila dužnost radnih smjena, metodi treba dati raspored. Stoga je svako izračunavanje na vremenskom rasporedu parametrizirano s dužnošću i rasporedom.
Prema zadanim postavkama, radno vrijeme je "uključeno", a raspored je zadani raspored satnice. Primjerice, ako nazovete count()
bez argumenata na nekom intervalu vremenskog okvira, dobit ćete broj radnih smjena u intervalu koji su deklarirani po zadanom rasporedu. Te zadane postavke olakšavaju život jer ćete se u praksi uglavnom htjeti baviti dežurnim radnim smjenama.
API
Cjelokupna dokumentacija o rasporedu dostupna je na stranici Read the Docs.
Paket se može instalirati na uobičajeni način pip install timeboard
.
Postavite vremenski raspored
Najjednostavniji način za početak je korištenje unaprijed konfiguriranog kalendara koji se isporučuje s paketom. Uzmimo redoviti kalendar radnih dana za Sjedinjene Države.
>>> import timeboard.calendars.US as US >>> clnd = US.Weekly8x5()
clnd
objekt je vremenski raspored (primjer timeboard.Timeboard
klase). Ima samo jedan zadani raspored koji odabire radne dane kao dežurne radne smjene, dok se vikendi, kao i promatranje američkih saveznih praznika, proglašavaju izvan službe.
Alati za izradu vlastitog vremenskog plana bit će kratko pregledani kasnije nakon što pogledamo što možete učiniti s vremenskim rasporedom.
Poigrajte se radnim pomacima
Pozivanje instance vremenskog odbora clnd()
s jednom vremenskom točkom dohvaća radni pomak koji sadrži tu točku. Kako imate radni pomak možete postaviti upit o njegovoj dužnosti:
Je li određeni datum radni dan?
>>> ws = clnd('27 May 2017')>>> ws.is_on_duty()False
Doista, bila je subota.
Također možete pogledati budućnost ili prošlost sa trenutne radne promjene:
Kada je bio sljedeći radni dan?
>>> ws.rollforward()Workshift(6359) of 'D' at 2017–05–30
The returned workshift has the sequence number of 6359 and represents the day of 30 May 2017, which, by the way, was the Tuesday after the Memorial Day holiday.
If we were to finish the project in 22 business days starting on 01 May 2017, when would be our deadline?
>>> clnd('01 May 2017') + 22Workshift(6361) of 'D' at 2017–06–01
This is the same as:
>>> clnd('01 May 2017').rollforward(22)Workshift(6361) of 'D' at 2017–06–01
Play with intervals
Calling clnd()
with a different set of parameters produces an object representing an interval on the calendar. The interval below contains all workshifts of the month of May 2017:
>>> may2017 = clnd('May 2017', period="M")
How many business days were there in May?
>>> may2017.count()22
How many days off?
>>> may2017.count(duty='off')9
How many working hours?
>>> may2017.worktime()176
An employee was on the staff from April 3, 2017, to May 15, 2017. What portion of April’s salary did the company owe them?
Note that calling clnd()
with a tuple of two points in time produces an interval containing all workshifts between these points, inclusively.
>>> time_in_company = clnd(('03 Apr 2017','15 May 2017'))>>> time_in_company.what_portion_of(clnd('Apr 2017', period="M"))1.0
Indeed, the 1st and the 2nd of April in 2017 fell on the weekend, therefore, having started on the 3rd, the employee checked out all the working days in the month.
And what portion of May’s?
>>> time_in_company.what_portion_of(may2017)0.5
How many days had the employee worked in May?
The multiplication operator returns the intersection of two intervals.
>>> (time_in_company * may2017).count()11
How many hours?
>>> (time_in_company * may2017).worktime()88
An employee was on the staff from 01 Jan 2016 to 15 Jul 2017. How many years had this person worked for the company?
>>> clnd(('01 Jan 2016', '15 Jul 2017')).count_periods('A')1.5421686746987953

Build your own timeboard
For the purpose of introduction, I will just plunge into two examples. If it seems too steep, please, find the thorough discussion of the construction tools in the project documentation.
The import statement for this section:
>>> import timeboard as tb
Let me return to a schedule of workshifts in the car dealership which I mentioned in the prologue. A mechanic works on Monday, Tuesday, Saturday, and Sunday this week, and on Wednesday, Thursday, and Friday next week; then the bi-weekly cycle repeats. The timeboard is created by the following code:
>>> biweekly = tb.Organizer(marker='W',... structure=[[1,1,0,0,0,1,1], [0,0,1,1,1,0,0]])>>> clnd = tb.Timeboard(base_unit_freq='D', ... start="01 Oct 2017", end="31 Dec 2018", ... layout=biweekly)
It makes sense to look into the last statement first. It creates a timeboard named clnd
. The first three parameters define the frame to be a sequence of days (‘D
’) from 01 Oct 2017 to 31 Dec 2018. The layout
parameter tells how to organize the frame into the timeline of workshifts. This job is commissioned to an Organizer
named biweekly
.
The first statement creates this Organizer
which takes two parameters: marker
and structure
. We use amarker
to place marks on the frame. The marks are kind of milestones which divide the frame into subframes, or “spans”. In the example marker=’W’
puts a mark at the beginning of each calendar week. Therefore, each span represents a week.
The structure
parameter tells how to create workshifts within each span. The first element of structure
, the list [1,1,0,0,0,1,1]
, is applied to the first span (i.e. to the first week of our calendar). Each base unit (that is, each day) within the span becomes a workshift. The workshifts receive labels from the list, in order.
The second element of structure
, the list [0,0,1,1,1,0,0]
, is analogously applied to the second span (the second week). After this, since we’ve gotten no more elements, a structure
is replayed in cycles. Hence, the third week is serviced by the first element of structure
, the fourth week by the second, and so on.
As a result, our timeline becomes the sequence of days labeled with the number 1
when the mechanic is on duty and with the number 0
when he or she is not. We have not specified any schedule, because the schedule which is built by default suits us fine. The default schedule considers the boolean value of the label, so 1
translates into ‘on duty’, and zero into ‘off duty’.
With this timeboard, we can do any type of calculations that we have done earlier with the business calendar. For example, if a person was employed to this schedule from November 4, 2017, and salary is paid monthly, what portion of November’s salary has the employee earned?
>>> time_in_company = clnd(('4 Nov 2017', None))>>> nov2017 = clnd('Nov 2017', period="M")>>> time_in_company.what_portion_of(nov2017)0.8125
In the second example we will build a timeboard for a call center. The call center operates round-the-clock in shifts of varying length: 08:00 to 18:00 (10 hours), 18:00 to 02:00 (8 hours), and 02:00 to 08:00 (6 hours). An operator’s schedule consists of one on-duty shift followed by three off-duty shifts. Hence, four teams of operators are needed. They are designated as ‘A’, ‘B’, ‘C’, and ‘D’.
>>> day_parts = tb.Marker(each='D', ... at=[{'hours':2}, {'hours':8}, {'hours':18}])>>> shifts = tb.Organizer(marker=day_parts, ... structure=['A', 'B', 'C', 'D'])>>> clnd = tb.Timeboard(base_unit_freq='H', ... start="01 Jan 2009 02:00", end="01 Jan 2019 01:59",... layout=shifts)>>> clnd.add_schedule(name='team_A', ... selector=lambda label: label=='A')
There are four key differences from the dealership case. We will examine them one by one.
First, the frame’s base unit is now a one-hour period (base_unit_freq='H'
) instead of a one-day period of the dealership’s calendar.
Second, the value of the marker
parameter of the Organizer is now a complex object instead of a single calendar frequency it was before. This object is an instance of Marker
class. It is used to define rules for placing marks on the frame when the simple division of the frame into uniform calendar units is not sufficient. The signature of the Marker above is almost readable — it says: place a mark on each day (‘D’) at 02:00 hours, 08:00 hours, and 18:00 hours.
Third, the value of the structure
is now simpler: it is a one-level list of teams’ labels. When an element of the structure
is not an iterable of labels but just one label, its application to a span produces a single workshift which, literally, spans the span.
In our example, the very first span comprises six one-hour base units starting at 2, 3, 4 … 7 o’clock in the morning of 01 Jan 2009. All these base units are combined into the single workshift with label ‘A’. The second span comprises ten one-hour base units starting at 8, 9, 10 … 17 o’clock. These base units are combined into the single workshift with label ‘B’, and so on. When all labels have been taken, the structure is replayed, so the fifth span (08:00:00–17:59:59 on 01 Jan 2009) becomes a workshift with label ‘A’.
To recap, if an element of structure
is a list of labels, each base unit of the span becomes a workshift and receives a label from the list. If an element of structure
is a single label, all base units of the span are combined to form a single workshift which receives this label.
And finally, we explicitly created a schedule for team A. The default schedule does not serve our purpose as it returns “always on duty”. This is true for the call center as a whole but not so for a particular team. For the new schedule, we supply the name and the selector function which returns True for all workshifts labeled with ‘A’. For the practical use, you will want to create the schedules for the other teams as well.
This timeboard is as good to work with as any other. However, this time we will have to explicitly specify the schedule we want to use.
>>> schedule_A = clnd.schedules['team_A']
How many shifts did the operators of team A sit in November 2017?
>>> nov2017 = clnd('Nov 2017', period="M", schedule=schedule_A)>>> nov2017.count()22
And how many hours were there in total?
>>> nov2017.worktime()176
A person was employed as an operator in team A from November 4, 2017. Salary is paid monthly. What portion of November’s salary has the employee earned?
>>> time_in_company = clnd(('4 Nov 2017',None), schedule=schedule_A)>>> time_in_company.what_portion_of(nov2017)0.9090909090909091
More use cases
You can find more use cases (taken almost from real life) in the jupyter notebook which is the part of the project documentation.
Please feel free to use timeboard
and do not hesitate to leave feedback or open issues on GitHub .