Gør din komplekse planlægning enkel med timeboard, et Python-bibliotek

timeboarder et Python-bibliotek, der opretter tidsplaner for arbejdsperioder og udfører kalenderberegninger over dem. Du kan oprette standard arbejdsdagskalendere såvel som en række andre tidsplaner, enkle eller komplekse.

Du kan finde dokumentationen her.

Tjek GitHub repo her.

Find det på PyPI her.

Historien

Det startede med antallet af medarbejdere. Vores firma introducerede KPI'er, der involverede indtægter pr. Medarbejder, så vi havde brug for at kende det gennemsnitlige årlige antal ansatte for hvert hold. Jeg havde allerede skrevet Python-scripts, så jeg blev ikke skræmt.

For at få et antal ansatte var jeg nødt til at beregne antallet af hverdage, hver medarbejder har brugt hos virksomheden inden for året. Pandaer ville klare det på et øjeblik, tænkte jeg. Men det kom ud, at Pandas ikke kunne.

Den russiske forretningskalender er besværlig. De bytter hverdage med lørdag eller søndag for at udfylde hullerne mellem helligdage og weekender. For eksempel skal du komme på arbejde en lørdag i februar for at få godtgjort en gratis mandag forud for en ferie tirsdag et eller andet sted i maj.

Ordningen for hvert år er unik. Pandas 'hverdagskalender understøtter kun envejsændringer til ferieobservationer. Så jeg kunne gøre en arbejdsdag til en fridag, men ikke omvendt.

Så var der operatører i callcenteret, og min angst svingede den anden vej. De arbejder i skift af forskellig længde og et skift efterfulgt af tre skift ud. For at få callcenter-statistikkerne havde jeg ikke brug for hverdagskalenderen. Alligevel måtte jeg tælle antallet af bestemte operatørskift i en periode.

Og endelig et offbeat problem. I mit lokale Honda-forhandler arbejder mekanikerne på alternative ugentlige tidsplaner: mandag, tirsdag, lørdag og søndag i denne uge og onsdag til fredag ​​den næste uge. Jeg ønskede altid at blive betjent af en bestemt mekaniker, fordi den anden engang havde ødelagt bremserne. Jeg ønskede en enkel måde at bestemme det næste skift på "min" mekaniker.

Disse sager har et fælles fundament. Deres løsninger er afhængige af en tidsplan for "i tjeneste" og "uden tjeneste" perioder. Vi skal være i stand til at oprette forskellige strukturerede tidsplaner, der passer til forskellige forretningssager. Forespørgsler og beregninger, der køres over tidsplanen, skal skelne mellem perioder med "i tjeneste" og "uden tjeneste".

Jeg kunne ikke finde en Python-pakke, der gav midlerne til at oprette og stille spørgsmål til sådanne tidsplaner. Da det skete, havde jeg lidt fritid til at skrive det selv.

Konceptet

timeboarder et Python-bibliotek, der opretter tidsplaner for arbejdsperioder og udfører kalenderberegninger over dem. Disse objekter kaldes selv timeboards.

Der er tre hovedtrin i ræsonnementet omkring en timeboard.

Du starter med et tidsinterval, der sætter grænserne for din kalender. Alt vil være begrænset til dette interval. Det kaldes (reference) rammen. Rammen består af basisenheder. En baseenhed er den mindste periode, du har brug for til at måle din kalender. For eksempel, hvis du ræsonnerer med hensyn til hverdage, er basisenheden en dag. Alternativt, hvis du opbygger en tidsplan for flere timers skift, er basisenheden en time.

På det næste trin definerer du reglerne for markering af rammen til arbejdsskift. Arbejdsskift er perioder, du holder af. De udgør din kalender. Det er arbejdsskift, du vil planlægge eller tælle. I en standard arbejdsdagskalender er workshift en dag (og basisenheden er også en dag, så de falder sammen).

I et callcenter er arbejdsskift en periode på flere timer, hvor et bestemt skift af operatører er på vagt. Basisenheden er sandsynligvis en time, og hver arbejdsskift omfatter et (sandsynligvis varierende) antal basisenheder.

Sekvensen af ​​arbejdsskift, der udfylder rammen, kaldes tidslinjen.

Endelig opretter du en eller flere tidsplaner. En tidsplan er som en stencil lagt over tidslinjen. Dens formål er at fortælle arbejdsskift, der er på vagt, fra arbejdsfri.

En tidsplan har brug for noget at arbejde med for at kunne erklære en arbejdsskift på vagt eller uden tjeneste. Dette er grunden til, at du angiver en etiket til hver arbejdsskift, eller rettere en regel for mærkning af dem, mens rammen er markeret i tidslinjen. Hver tidsplan definerer en vælgerfunktion, der inspicerer workshifts etiket og returnerer True for arbejdsskiftene på arbejdspladsen og ellers Falsk. Medmindre du tilsidesætter den, ledsages en tidslinje af standardplanen, hvis vælger returnerer etikettens boolske værdi.

Nogle gange vil du definere flere tidsplaner for den samme tidslinje. For eksempel vil der i et callcenter være tidsplanen for callcenteret som helhed og en separat tidsplan for hvert team af operatører. Den samme arbejdsskift kan findes på vagt under nogle tidsplaner og uden vagt under de andre.

Timeboard = tidslinje + tidsplaner. Mere præcist timeboard er en samling af arbejdet tidsplaner baseret på en specifik tidslinje af workshifts bygget på en reference ramme .

Når du har fået et timeboard, kan du udføre det nyttige arbejde: foretage kalenderberegninger for at løse de problemer som dem, der er beskrevet i prologen.

Hver beregning, der udføres med timeboard, er pligtbevidst. Den påkaldte metode “ser” arbejder kun forskydninger med den specificerede pligt og ignorerer de andre. For at afsløre arbejdsskiftets pligt skal metoden gives en tidsplan. Derfor parametriseres hver beregning på timeboardet med en pligt og en tidsplan.

Som standard er afgiften "til", og tidsplanen er standardplanen for timeboardet. For eksempel, hvis du ringer count()uden argumenter på et tidsinterval, får du antallet af arbejdsskift i intervallet, der erklæres på vagt under standardplanen. Disse standarder gør livet lettere, fordi du i praksis mest gerne vil beskæftige dig med arbejdsskift.

API'et

Den komplette tidsdokumentation er tilgængelig på Læs Dokumenterne.

Pakken kan installeres med det sædvanlige pip install timeboard.

Opret et timeboard

Den enkleste måde at komme i gang er at bruge en forudkonfigureret kalender, der følger med pakken. Lad os tage en regelmæssig hverdagskalender for USA.

 >>> import timeboard.calendars.US as US >>> clnd = US.Weekly8x5()

clnd object is a timeboard (an instance of timeboard.Timeboard class). It has only one default schedule which selects weekdays as on-duty workshifts while weekends, as well as observations of US federal holidays, are declared off duty.

The tools for building your own timeboard will be briefly reviewed later on after we look at what you can do with a timeboard.

Play with workshifts

Calling a timeboard instance clnd() with a single point in time retrieves the workshift that contains this point. How that you have a workshift you can query its duty:

Is a certain date a business day?

>>> ws = clnd('27 May 2017')>>> ws.is_on_duty()False

Indeed, it was a Saturday.

You can also look into the future or in the past from the current workshift:

When was the next business day?

>>> 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']

Hvor mange skift sad operatørerne af hold A i november 2017?

>>> nov2017 = clnd('Nov 2017', period="M", schedule=schedule_A)>>> nov2017.count()22

Og hvor mange timer var der i alt?

>>> nov2017.worktime()176

En person blev ansat som operatør i team A fra 4. november 2017. Løn udbetales månedligt. Hvilken del af novemberlønnen har medarbejderen tjent?

>>> time_in_company = clnd(('4 Nov 2017',None), schedule=schedule_A)>>> time_in_company.what_portion_of(nov2017)0.9090909090909091

Flere brugssager

Du kan finde flere brugssager (taget næsten fra det virkelige liv) i jupyter-notesbogen, som er den del af projektdokumentationen.

Du er velkommen til at bruge timeboardog tøv ikke med at give feedback eller åbne problemer på GitHub.