Nedávno jsem implementoval algoritmus pro kontrolu, zda rozvržení pracovních směn splňuje zákonnou dobu odpočinku mezi směnami a nepřetržitého odpočinku v týdnu dle legislativy České republiky. Algoritmus je součástí docházkového systému vyvíjeného naší společností. Myslím, že je tento algoritmus sám o sobě docela zajímavý, ale hlavně je to dobrý příklad přímo z praxe na demonstrací, jak může být netriviální celý proces od zadání (v tomto případě v podobě zákoníku práce) až po výsledný kód.
Vlastně se jedná o nezávislé kontroly dvě - tedy dva algoritmy, které vycházejí z popisu v zákoníku práce (zákon č. 262/2006 Hl. IV, díl 1) dle § 90 (nepřetržitý odpočinek mezi dvěma směnami) resp. § 92 (nepřetržitý odpočinek v týdnu). V této sérii uvedu pouze implementaci algoritmu druhého, protože je složitější a pro nás tudíž i zajímavější.
Nejprve vás stručně seznámím s k tomuto relevantními záležitostmi týkajících se docházkového systému. V systému existují tzv. kalendáře. Každá osoba má aktuálně přiřazen jeden konkrétní kalendář. Každý kalendář jednak obsahuje definici pracovních směn a jednak umožňují nastavit konkrétní hodnoty v systému definovaných parametrů, které ovlivňují vlastní zpracování docházky pro danou osobu. Každá osoba pak ještě může mít definované nějaké výjimky kalendáře tj. změny v definici pracovních směn oproti kalendáři. Business vrstva aplikační logiky pak umožňuje:
- Pro každou osobu a měsíc vrátit pracovní směny (s datem a časem začátku i konce dané směny) včetně zahrnutí všech výjimek kalendáře pro jednotlivé dny měsíce.
- Pro každou osobu, datum a název parametru vrátit konkrétní hodnotu parametru kalendáře.
Z § 92 zákoníku práce je pro vlastní algoritmus důležité pouze toto:
- Zaměstnavatel je povinen rozvrhnout pracovní dobu tak, aby zaměstnanec měl nepřetržitý odpočinek v týdnu během každého období sedmi po sobě jdoucích kalendářních dnů (*) v trvání alespoň 35 hodin (u mladistvého zaměstnance je tato hodnota 48 hodin).
- V některých případech (**) lze tento nepřetržitý odpočinek v týdnu zaměstnancům zkrátit až na 24 hodiny s tím, že zaměstnancům bude poskytnut nepřetržitý odpočinek v týdnu tak, aby za období 2 týdnů činila délka tohoto odpočinku celkem alespoň 70 hodin (***) (toto ustanovení neplatí pro mladistvé).
A můžeme pomalu začít. Vlastní kontrola bude funkce, která se bude spouštět pro konkrétní osobu (zaměstnance) identifikovanou pomoci svého IDOsoby a dále měsíc/rok. Výstupem bude kolekce údajů o nalezeném nesouladu s legislativou. Tyto údaje budou:
- Datum od a datum do určující, k jakému časovému intervalu se nesoulad vztahuje (pro tuto kontrolu se bude vždy jednat o týden).
- Rozlišení, o který nesoulad se jedná (pro tuto kontrolu půjde vždy o nepřetržitý odpočinek v týdnu).
- Popis nesouladu pro zobrazení do uživatelského rozhraní. Ten bude realizovaný jako funkce (Func<string>) z toho důvodu, aby byla prováděna jeho lokalizace do příslušné kultury až v momentě jeho zobrazování v uživatelském rozhraní (resp. přesněji se tak například bude dít v momentě jeho serializace na klienta).
/// <summary>
/// Typ kontroly zákonné doby odpočinku
/// </summary>
public enum PracovniCasyValidationType
{
/// <summary>
/// Nepřetržitý odpočinek v týdnu
/// </summary>
NepretrzityOdpocinekVTydnu = 1
}
/// <summary>
/// Konkrétní chyba při kontrole zákonné doby odpočinku
/// </summary>
public sealed class PracovniCasyValidationError
{
#region member varible and default property initialization
public PracovniCasyValidationType Type { get; private set; }
public DateTime DatumOd { get; private set; }
public DateTime DatumDo { get; private set; }
private Func<string> PopisSelector;
#endregion
#region constructors and destructors
internal PracovniCasyValidationError(PracovniCasyValidationType type, DateTime datumOd, DateTime datumDo, Func<string> popisSelector)
{
this.Type = type;
this.DatumOd = datumOd;
this.DatumDo = datumDo;
this.PopisSelector = popisSelector;
}
#endregion
#region action methods
public override string ToString()
{
return GetPopis();
}
public string GetPopis()
{
return this.PopisSelector();
}
#endregion
}
/// <summary>
/// Kontrola dodržení zákonné doby odpočinku
/// </summary>
public static class PracovniCasyValidator
{
#region action methods
public static IEnumerable<PracovniCasyValidationError> Validate(int IDOsoby, int rok, int mesic)
{
return KontrolaNepretrzityOdpocinekVTydnu(IDOsoby, rok, mesic);
}
#endregion
#region private member functions
private static IEnumerable<PracovniCasyValidationError> KontrolaNepretrzityOdpocinekVTydnu(int IDOsoby, int rok, int mesic)
{
//TODO: Kontrola nepřetržitého odpočinku v týdnu
}
#endregion
}
Z hlediska případné změny časů pro různé režimy nebo budoucí změny legislativy a hlavně z důvodů vyřešení odlišných podmínek pro mladistvé budou konkrétní hodnoty, které se vyskytují ve výše uvedeném změní ze zákoníku, zavedeny jako parametry kalendáře a uvedené hodnoty budou jejich výchozí hodnoty. Bude se jednat o tyto parametry:
- nepretrzityOdpocinek - Nepřetržitý odpočinek (35 hodin)
- nepretrzityOdpocinekMin - Minimální nepřetržitý odpočinek (24 hodin)
- nepretrzityOdpocinekDodatecny - Nepřetržitý odpočinek dodatečný (70 hodin)
Kód kterým půjde tyto hodnoty získat bude následující:
int nepretrzityOdpocinek = Nastaveni.Get().GetInt("NepretrzityOdpocinek", IDOsoby, new DateTime(rok, mesic, 1)); //35
int nepretrzityOdpocinekMin = Nastaveni.Get().GetInt("NepretrzityOdpocinekMin", IDOsoby, new DateTime(rok, mesic, 1)); //24
int nepretrzityOdpocinekDodatecny = Nastaveni.Get().GetInt("NepretrzityOdpocinekDodatecny", IDOsoby, new DateTime(rok, mesic, 1)); //70
Kde objekt business vrstvy Nastavení můžeme simulovat takto:
internal sealed class Nastaveni
{
#region member varible and default property initialization
private static readonly Nastaveni s_instance = new Nastaveni();
#endregion
#region constructors and destructors
private Nastaveni() { }
#endregion
#region action methods
public static Nastaveni Get()
{
return s_instance;
}
public int GetInt(string parametr, int IDOsoby, DateTime datum)
{
switch (parametr)
{
case "NepretrzityOdpocinek":
return 35;
case "NepretrzityOdpocinekMin":
return 24;
case "NepretrzityOdpocinekDodatecny":
return 70;
}
throw new ArgumentException("Parametr kalendáře '{0}' není definován.");
}
#endregion
}
Protože algoritmus bude muset procházet pracovní směny dané osoby, připravíme si nyní ještě i toto volání business vrstvy. Jak bylo výše uvedeno business vrstva umožňuje pro osobu a měsíc/rok získat směny pro jednotlivé dny měsíce.
Volání, kterým lze například získat posloupnost všech pracovních směn v měsíci vypadá pak následovně:
var smeny = from den in PracovniCasyOsobaMesic.Get(IDOsoby, rok, mesic).JednotliveDny
from smena in den.Smeny
select smena;
Objekt business vrstvy PracovniCasyOsobaMesic budeme opět jen simulovat, například takto:
[System.Diagnostics.DebuggerDisplay("ZnackaSmeny = {ZnackaSmeny}, DatumACasOd = {DatumACasOd}, DatumACasDo = {DatumACasDo}")]
internal sealed class PracovniCasyOsobaDenSmena
{
#region member varible and default property initialization
public string ZnackaSmeny { get; private set; }
public DateTime DatumACasOd { get; private set; }
public DateTime DatumACasDo { get; private set; }
#endregion
#region constructors and destructors
internal PracovniCasyOsobaDenSmena(string znackaSmeny, DateTime datumACasOd, DateTime datumACasDo)
{
this.ZnackaSmeny = znackaSmeny;
this.DatumACasOd = datumACasOd;
this.DatumACasDo = datumACasDo;
}
#endregion
}
[System.Diagnostics.DebuggerDisplay("Datum = {Datum}, Smeny = {Smeny.Count}")]
internal sealed class PracovniCasyOsobaDen
{
#region member varible and default property initialization
public DateTime Datum { get; private set; }
public IList<PracovniCasyOsobaDenSmena> Smeny { get; private set; }
#endregion
#region constructors and destructors
internal PracovniCasyOsobaDen(DateTime datum, List<PracovniCasyOsobaDenSmena> smeny)
{
this.Datum = datum;
this.Smeny = smeny.AsReadOnly();
}
#endregion
}
internal class PracovniCasyOsobaMesic
{
#region member varible and default property initialization
public IList<PracovniCasyOsobaDen> JednotliveDny { get; private set; }
#endregion
#region constructors and destructors
private PracovniCasyOsobaMesic(List<PracovniCasyOsobaDen> jednotliveDny)
{
this.JednotliveDny = jednotliveDny.AsReadOnly();
}
#endregion
#region action methods
public static PracovniCasyOsobaMesic Get(int IDOsoby, int rok, int mesic)
{
string rozpisSmen = "R--RRRRR-RRRRRRRRRRRR-RRRRR";
return FromRozpisSmen(rok, mesic, rozpisSmen);
}
#endregion
#region private member functions
private static PracovniCasyOsobaMesic FromRozpisSmen(int rok, int mesic, string rozpisSmen)
{
DateTime datumOd = new DateTime(rok, mesic, 1);
DateTime datumDo = datumOd.AddMonths(1).AddDays(-1);
var typSmen = new Dictionary<string, Tuple<TimeSpan, TimeSpan>>()
{
{ "R", Tuple.Create(new TimeSpan(8, 0, 0), new TimeSpan(16, 0, 0)) }
{ "V", Tuple.Create(new TimeSpan(8, 0, 0), new TimeSpan(18, 0, 0)) }
};
var jednotliveDny = new List<PracovniCasyOsobaDen>(31);
int index = 0;
for (DateTime datum = datumOd; datum <= datumDo; datum = datum.AddDays(1))
{
var smeny = new List<PracovniCasyOsobaDenSmena>();
string znackaSmeny = rozpisSmen[index++ % rozpisSmen.Length].ToString();
Tuple<TimeSpan, TimeSpan> casy;
if (typSmen.TryGetValue(znackaSmeny, out casy))
{
smeny.Add(new PracovniCasyOsobaDenSmena(znackaSmeny, datum.Add(casy.Item1), datum.Add(casy.Item2)));
}
jednotliveDny.Add(new PracovniCasyOsobaDen(datum, smeny));
}
return new PracovniCasyOsobaMesic(jednotliveDny);
}
#endregion
}
Při této simulaci bude mít každý den v měsíci pouze jednu nebo žádnou směnu. V reálu to tak být nemusí, ale pro ověření našeho algoritmu je toto plně postačující.
Příště budeme pokračovat již implementací vlastního algoritmu.
(*) Z původního znění zákoníku práce byla tato klauzule „během každého období sedmi po sobě jdoucích kalendářních dnů“ vypuštěna. Jak s tímto naložíme se dozvíme později.
(**) To v kterých případech konkrétně pro nás není až tak důležité, důležité je ale to, že musíme uvažovat, že mohou existovat různé režimy a pro některé se tato podmínka má a pro některé nemá uplatnit. To samé platí ohledně rozlišení, zda je zaměstnanec mladistvý či nikoliv.
(***) Tento výklad navíc doplňuje: První odpočinek 24 hodin, druhý odpočinek 46 hodin (24 + 35 + 11=70).