Κάντε τον περίπλοκο προγραμματισμό σας απλό με το timeboard, μια βιβλιοθήκη Python

timeboardείναι μια βιβλιοθήκη Python που δημιουργεί χρονοδιαγράμματα περιόδων εργασίας και εκτελεί υπολογισμούς ημερολογίου πάνω τους. Μπορείτε να δημιουργήσετε τυπικά ημερολόγια εργάσιμης ημέρας, καθώς και διάφορα άλλα προγράμματα, απλά ή πολύπλοκα.

Μπορείτε να βρείτε την Τεκμηρίωση εδώ.

Δείτε το repo GitHub εδώ.

Βρείτε το στο PyPI εδώ.

Η ιστορία

Ξεκίνησε με τη θήκη headcount. Η εταιρεία μας εισήγαγε KPI με έσοδα ανά εργαζόμενο, οπότε χρειαζόμασταν να γνωρίζουμε τον μέσο ετήσιο αριθμό κάθε ομάδας. Είχα ήδη γράψει σενάρια Python, οπότε δεν με φοβόταν.

Για να αποκτήσω έναν αριθμό προσωπικού έπρεπε να υπολογίσω τον αριθμό των εργάσιμων ημερών που κάθε υπάλληλος πέρασε με την εταιρεία εντός του έτους. Πάντα θα το χειριζόταν σε ένα δευτερόλεπτο, σκέφτηκα. Αλλά βγήκε ότι οι Πάντες δεν μπορούσαν.

Το ρωσικό επιχειρηματικό ημερολόγιο είναι δυσκίνητο. Ανταλλάσσουν τις καθημερινές με τα Σάββατα ή τις Κυριακές για να καλύψουν τα κενά μεταξύ αργιών και σαββατοκύριακων. Για παράδειγμα, πρέπει να έρθετε στη δουλειά το Σάββατο του Φεβρουαρίου για να αποζημιωθείτε με μια δωρεάν Δευτέρα πριν από τις διακοπές την Τρίτη κάπου τον Μάιο.

Το πρόγραμμα για κάθε έτος είναι μοναδικό. Το ημερολόγιο της εργάσιμης ημέρας του Pandas υποστηρίζει μόνο τροποποιήσεις μίας κατεύθυνσης για παρατηρήσεις διακοπών. Επομένως, θα μπορούσα να μετατρέψω μια εργάσιμη ημέρα σε μια ημέρα διακοπών, αλλά όχι το αντίστροφο.

Τότε υπήρχαν χειριστές στο τηλεφωνικό κέντρο και το άγχος μου άλλαξε. Λειτουργούν σε βάρδιες ποικίλου μήκους και μία βάρδια ακολουθούμενη από τρεις βάρδιες. Για να λάβω τα στατιστικά του τηλεφωνικού κέντρου, δεν χρειάζομαι το ημερολόγιο της εργάσιμης ημέρας. Ωστόσο, έπρεπε να μετρήσω τον αριθμό των αλλαγών συγκεκριμένου χειριστή σε μια χρονική περίοδο.

Και τέλος, ένα απίστευτο πρόβλημα. Στην τοπική αντιπροσωπεία της Honda, οι μηχανικοί εργάζονται με εναλλακτικά εβδομαδιαία δρομολόγια: Δευτέρα, Τρίτη, Σάββατο και Κυριακή αυτήν την εβδομάδα και Τετάρτη έως Παρασκευή την επόμενη εβδομάδα. Ήθελα να σερβίρονται πάντα από έναν συγκεκριμένο μηχανικό, γιατί ο άλλος είχε κάποτε χάσει τα φρένα. Ήθελα έναν απλό τρόπο να προσδιορίσω την επόμενη αλλαγή του μηχανικού «μου».

Αυτές οι περιπτώσεις έχουν ένα κοινό θεμέλιο. Οι λύσεις τους θα βασίζονταν σε ένα χρονοδιάγραμμα “on-duty” και “off-duty” χρονικών περιόδων. Πρέπει να είμαστε σε θέση να κατασκευάσουμε διάφορα δομημένα χρονοδιαγράμματα κατάλληλα για διαφορετικές επιχειρηματικές περιπτώσεις. Τα ερωτήματα και οι υπολογισμοί που εκτελούνται κατά τη διάρκεια του χρονοδιαγράμματος πρέπει να διακρίνουν μεταξύ των περιόδων «εν υπηρεσία» και «εκτός υπηρεσίας».

Δεν μπόρεσα να βρω ένα πακέτο Python που παρείχε τα μέσα για την κατασκευή και την αναζήτηση τέτοιων προγραμμάτων. Όπως συνέβη, είχα λίγο ελεύθερο χρόνο για να το γράψω μόνος μου.

Η ιδέα

timeboardείναι μια βιβλιοθήκη Python που δημιουργεί χρονοδιαγράμματα περιόδων εργασίας και εκτελεί υπολογισμούς ημερολογίου πάνω τους. Αυτά τα ίδια τα αντικείμενα ονομάζονται timeboards.

Υπάρχουν τρία μεγάλα βήματα στο σκεπτικό σχετικά με ένα ταμπλό.

Ξεκινάτε με ένα χρονικό διάστημα που καθορίζει τα όρια του ημερολογίου σας. Όλα θα περιοριστούν σε αυτό το διάστημα. Ονομάζεται πλαίσιο (αναφοράς). Το πλαίσιο αποτελείται από βασικές μονάδες. Η βασική μονάδα είναι η μικρότερη χρονική περίοδος που χρειάζεστε για τη μέτρηση του ημερολογίου σας. Για παράδειγμα, εάν έχετε λόγο για εργάσιμες ημέρες, τότε η βασική μονάδα είναι μια ημέρα. Εναλλακτικά, εάν δημιουργήσετε ένα πρόγραμμα πολλαπλών ωρών βάρδιες, τότε η βασική μονάδα είναι μία ώρα.

Στο επόμενο βήμα, ορίζετε τους κανόνες σήμανσης του πλαισίου σε εργαστήρια. Τα εργαστήρια είναι χρονικές περιόδους που σας ενδιαφέρουν. Αποτελούν το ημερολόγιό σας. Είναι εργαστήρια που θέλετε να προγραμματίσετε ή να μετρήσετε. Σε ένα τυπικό ημερολόγιο εργάσιμων ημερών, το εργαστήριο είναι μια ημέρα (και η βασική μονάδα είναι επίσης μια ημέρα, έτσι συμπίπτουν).

Σε ένα τηλεφωνικό κέντρο, η εργασιακή μετατόπιση είναι μια περίοδος αρκετών ωρών όταν μια συγκεκριμένη μετατόπιση των χειριστών είναι σε λειτουργία. Η βασική μονάδα είναι πιθανό να είναι μία ώρα και κάθε εργαστήριο περιλαμβάνει έναν (πιθανώς διαφορετικό) αριθμό βασικών μονάδων.

Η ακολουθία των εργαστηρίων που γεμίζουν το πλαίσιο ονομάζεται χρονοδιάγραμμα.

Τέλος, δημιουργείτε ένα ή περισσότερα προγράμματα. Ένα πρόγραμμα είναι σαν ένα διάτρητο που βρίσκεται πάνω στο χρονοδιάγραμμα. Σκοπός του είναι να πει σε εργαστηριακά εργαστήρια από εκτός καθηκόντων.

Ένα πρόγραμμα χρειάζεται κάτι για να δουλέψει για να δηλώσει μια εργασιακή αλλαγή σε καθήκον ή εκτός υπηρεσίας. Αυτός είναι ο λόγος για τον οποίο παρέχετε μια ετικέτα για κάθε εργαστήριο, ή μάλλον έναν κανόνα για την επισήμανσή τους ενώ το πλαίσιο επισημαίνεται στο χρονοδιάγραμμα. Κάθε πρόγραμμα καθορίζει μια λειτουργία επιλογής που ελέγχει την ετικέτα του εργαστηρίου και επιστρέφει την τιμή True για τα εργαστήρια κατά την εργασία και False διαφορετικά. Εάν δεν το παρακάμψετε, ένα χρονοδιάγραμμα συνοδεύεται από το προεπιλεγμένο πρόγραμμα του οποίου ο επιλογέας επιστρέφει τη δυαδική τιμή της ετικέτας.

Μερικές φορές θέλετε να ορίσετε διάφορα προγράμματα για το ίδιο χρονοδιάγραμμα. Για παράδειγμα, σε ένα τηλεφωνικό κέντρο, θα υπάρχει το πρόγραμμα για το τηλεφωνικό κέντρο στο σύνολό του και ένα ξεχωριστό πρόγραμμα για κάθε ομάδα χειριστών. Το ίδιο εργαστήριο μπορεί να βρεθεί σε υπηρεσία κάτω από ορισμένα χρονοδιαγράμματα και εκτός υπηρεσίας κάτω από τα άλλα.

Timeboard = χρονοδιάγραμμα + χρονοδιαγράμματα. Πιο συγκεκριμένα, timeboard είναι μια συλλογή των εργασιών χρονοδιαγράμματα με βάση ένα συγκεκριμένο χρονοδιάγραμμα των workshifts χτισμένο σε μια αναφορά πλαισίου .

Μόλις λάβετε ένα ταμπλό, μπορείτε να πραγματοποιήσετε τη χρήσιμη εργασία: κάντε υπολογισμούς ημερολογίου για να λύσετε τα προβλήματα όπως αυτά που περιγράφονται στον πρόλογο.

Κάθε υπολογισμός που εκτελείται με το timeboard είναι ενήμερος για το καθήκον. Η επικαλούμενη μέθοδος "βλέπει" λειτουργεί μόνο με το καθορισμένο καθήκον και αγνοεί τους άλλους. Για να αποκαλυφθεί το καθήκον των εργαστηρίων, η μέθοδος πρέπει να δοθεί χρονοδιάγραμμα. Επομένως, κάθε υπολογισμός στο ταμπλό παραμετροποιείται με καθήκον και χρονοδιάγραμμα.

Από προεπιλογή, το καθήκον είναι "ενεργοποιημένο" και το χρονοδιάγραμμα είναι το προεπιλεγμένο πρόγραμμα του ταμπλό. Για παράδειγμα, εάν καλέσετε count()χωρίς ορίσματα σε κάποιο χρονικό διάστημα ενός timeboard, θα λάβετε τον αριθμό των εργαστηρίων στο διάστημα που δηλώνονται σε λειτουργία σύμφωνα με το προεπιλεγμένο πρόγραμμα. Αυτές οι προεπιλογές διευκολύνουν τη ζωή, διότι στην πράξη θα θέλετε να ασχοληθείτε κυρίως με εργαστήρια που βρίσκονται σε υπηρεσία.

Το API

Η πλήρης τεκμηρίωση του πίνακα ανακοινώσεων είναι διαθέσιμη στο Read the Docs.

Το πακέτο μπορεί να εγκατασταθεί με το συνηθισμένο pip install timeboard.

Ρυθμίστε ένα ταμπλό

Ο απλούστερος τρόπος για να ξεκινήσετε είναι να χρησιμοποιήσετε ένα προκαθορισμένο ημερολόγιο που αποστέλλεται με το πακέτο. Ας πάρουμε ένα κανονικό ημερολόγιο εργάσιμης ημέρας για τις Ηνωμένες Πολιτείες.

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

Πόσες βάρδιες κάθισαν οι χειριστές της ομάδας Α τον Νοέμβριο του 2017;

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

Και πόσες ώρες υπήρχαν συνολικά;

>>> nov2017.worktime()176

Ένα άτομο εργαζόταν ως χειριστής στην ομάδα Α από τις 4 Νοεμβρίου 2017. Ο μισθός καταβάλλεται κάθε μήνα. Ποιο μέρος του μισθού του Νοεμβρίου έχει κερδίσει ο υπάλληλος;

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

Περισσότερες περιπτώσεις χρήσης

Μπορείτε να βρείτε περισσότερες περιπτώσεις χρήσης (που λαμβάνονται σχεδόν από την πραγματική ζωή) στο σημειωματάριο jupyter που είναι το μέρος της τεκμηρίωσης του έργου.

Μη διστάσετε να χρησιμοποιήσετε timeboardκαι μην διστάσετε να αφήσετε σχόλια ή να ανοίξετε ζητήματα στο GitHub.