Kontrola nepřetržitého odpočinku v týdnu, část 2

Tomáš Holan       22.02.2013             10512 zobrazení

Minule jsme si připravili vše potřebné, aby bylo možné začít s vlastní implementací algoritmu pro kontrolu doby nepřetržitého odpočinku v týdnu. Zatím máme jen nějakou základní představu, že algoritmus bude muset procházet pracovní směny osoby resp. přesněji procházet a kontrolovat jednotlivé intervaly mezi koncem směny a začátkem směny následující pro nějaké týdny. Teď musíme vyřešit co znamená týden.

Protože z původního znění zákoníku práce, který byl také uveden minule, byla vypuštěna klauzule „během každého období sedmi po sobě jdoucích kalendářních dnů“, měl by algoritmus procházet postupně jednotlivé týdny v měsíci. Přitom ale zákoník práce nehovoří o tom, kdy týden začíná a kdy končí. Dalo by se vycházet z čistě kalendářních dnů a týden by mohl začínat pondělkem (nevadí že “natvrdo”, protože algoritmus je jen dle legislativy České republiky), v takovém případě by bylo potřeba zahájit procházení týdnů od pondělí, které předchází aktuální měsíc.

Pravdou ale je, že jsme takto algoritmus implementovali a poté se všichni naši zákazníci, kteří tuto funkci začali využívat, ozvali s tím, že pro praxi to takto vůbec nevyhovuje, a že potřebují, aby se kontrola prováděla dle původního změní zákona. I tady budeme tedy implementovat tuto variantu. V takovém případě “týden” pouze označuje posloupnost 7-mi po sobě jdoucích dnů (2 týdny je posloupnost 14-ti po sobě jdoucích dnů) a “každé období” znamená, že libovolná takováto posloupnost musí splňovat dané podmínky.

Z toho plyne následující mini-specifikace pomocné funkce, která bude vracet jaké týdny se budou procházet:

  • Vždy se vrátí jeden týden, který se bude kontrolovat vždy, a jeden týden následující za tímto týdnem. Druhý týden se ale bude kontrolovat jen v případě, kdy první týden nebude splňovat podmínku pro nepřetržitý odpočinek (35 hod).
  • Pro první týden se bude kontrolovat 7-po sobě jsoucích dnů na podmínku pro minimální nepřetržitý odpočinek (24 hod) a na podmínku pro nepřetržitý odpočinek (35 hod).
  • Pro druhý týden se bude kontrolovat 14-po sobě jsoucích dnů (od začátku týdne prvního) na podmínku pro dodatečný nepřetržitý odpočinek (70 hod). (14 dní je proto, aby byla zahrnuta celá mezera mezi těmito dvěma týdny).
  • Procházení začne 7 kalendářních dnů před začátkem aktuálního měsíce (aby při případném nesplnění podmínky pro nepřetržitý odpočinek byl první týden měsíce kontrolován na podmínku pro nepřetržitý odpočinek dodatečný).
  • Budou se kontrolovat všechny možné posloupnosti 7 (resp. 14) po sobě jdoucích dnů (tj. jednotlivé “týdny” budou mezi sebou posunuté pouze o 1 den).
  • Procházení skončí týdnem, který začíná posledním dnem v daném měsíci.

Nyní můžeme zkusit tuto pomocnou metodu implementovat. Metoda bude ve třídě PracovniCasyValidator:

private static IEnumerable<Tyden> EnumerateWeeks(int IDOsoby, int rok, int mesic)
{
    DateTime datumOd = new DateTime(rok, mesic, 1);
    DateTime datumDo = datumOd.AddMonths(1).AddDays(-1);

    //Varianta algoritmu uvažující každých 7 po sobě jdoucích kalendářních dnů

    //Procházení se začíná 7 kalendářních dnů před začátkem aktuálního měsíce tj. pokud těchto 7 dnů nebude splňovat podmínku pro nepřetržitý odpočinek (35 hod),
    //musí 7 dnů před začátkem aktuálního měsíce a prvních 7 dnů aktuálního měsíce splňovat podmínku pro dodatečný nepřetržitý odpočinek (70 hod) jinak
    //stačí, aby prvních 7 dnů aktuálního měsíce splnilo standardní podmínku pro minimální nepřetržitý odpočinek resp. nepřetržitý odpočinek (24 resp. 35 hod).

    //Zkouší se všechny dny od 7 kalendářních dnů před začátkem aktuálního měsíce až po ukončení měsíce, aby bylo splněno, že libovolných 7 (resp. 14) po sobě jdoucích dnů splňuje dané podmínky.
    DateTime datum = datumOd.AddDays(-7);
    while (datum <= datumDo)
    {
        yield return new Tyden(datum, false, EnumerateWeekIntervals(IDOsoby, datum, datum.AddDays(7)));               //Kontrola 7-mi dnů
        yield return new Tyden(datum.AddDays(7), true, EnumerateWeekIntervals(IDOsoby, datum, datum.AddDays(2 * 7))); //Kontrola 14-ti dnů v případě nesplnění nepretrzityOdpocinek

        datum = datum.AddDays(1);
    }
}

Kde pomocný objekt Tyden nese datum svého začátku, boolean příznak zda se jedná o “první” či “druhý” týden a intervaly, které se budou kontrolovat. Pozor, že “druhý” týden sice začíná o 7 dní po týdnu “prvním”, ale bude obsahovat intervaly za celých 14 dní. Ještě jednou, to je z toho důvodu, aby při jeho kontrole byla uvažována celá případná mezera mezi “prvním” a “druhým” týdnem.

Třída Tyden bude nested ve třídě PracovniCasyValidator:

[System.Diagnostics.DebuggerDisplay("DatumOd = {DatumOd}, DatumDo = {DatumDo}, KontrolovatPouzeOdpocinekDodatecny = {KontrolovatPouzeOdpocinekDodatecny}")]
private sealed class Tyden
{
    #region member varible and default property initialization
    /// <summary>
    /// První den týdne
    /// </summary>
    public DateTime DatumOd { get; private set; }

    /// <summary>
    /// Poslední den týdne
    /// </summary>
    public DateTime DatumDo { get; private set; }

    /// <summary>
    /// Příznak, že se má daný týden kontrolovat pouze v případě předešlého nesplnění nepretrzityOdpocinek
    /// </summary>
    public bool KontrolovatPouzeOdpocinekDodatecny { get; private set; }

    /// <summary>
    /// Intervaly mezi směnami, které patří do týdne
    /// </summary>
    public IEnumerable<TydenInterval> Intervaly { get; private set; }
    #endregion

    #region constructors and destructors
    public Tyden(DateTime datumOd, bool kontrolovatPouzeOdpocinekDodatecny, IEnumerable<TydenInterval> intervaly)
    {
        this.DatumOd = datumOd;
        this.DatumDo = datumOd.AddDays(6);
        this.KontrolovatPouzeOdpocinekDodatecny = kontrolovatPouzeOdpocinekDodatecny;
        this.Intervaly = intervaly.ToList();
    }
    #endregion
}

K tomu, aby byl kód výše funkční, nám v tuto chvíli ještě chybí objekt TydenInterval a metoda EnumerateWeekIntervals.

Objekt TydenInterval představuje interval mezi směnami, bude tedy určený svým počátkem (to bude datum a čas konce nějaké směny) a koncem (datum a čas začátku směny následující) a dále bude umožňovat zjistit svojí délku:

[System.Diagnostics.DebuggerDisplay("Pocatek = {Pocatek}, Konec = {Konec}, Delka = {Delka}")]
private sealed class TydenInterval
{
    #region member varible and default property initialization
    /// <summary>
    /// Datum a čas do předchozí směny
    /// </summary>
    public DateTime Pocatek { get; private set; }

    /// <summary>
    /// Datum a čas od následující směny
    /// </summary>
    public DateTime Konec { get; private set; }
    #endregion

    #region constructors and destructors
    public TydenInterval(DateTime pocatek, DateTime konec)
    {
        this.Pocatek = pocatek;
        this.Konec = konec;
    }
    #endregion

    #region property getters/setters
    public TimeSpan Delka
    {
        get { return this.Konec - this.Pocatek; }
    }
    #endregion
}

Metoda EnumerateWeekIntervals má za úkol vrátit posloupnost intervalů mezi jednotlivými pracovními směnami pro danou osobu a pro obecný časový úsek od, do (7 resp. 14 dnů). Její signatura bude:

private static IEnumerable<TydenInterval> EnumerateWeekIntervals(int IDOsoby, DateTime datumOd, DateTime datumDo)
{
    //TODO: Vrátit intervaly
}

Než přistoupíme k její implementaci opět si nejprve musíme napsat její specifikaci:

  • Metoda bude procházet pracovní směny, ale vracet bude intervaly mezi těmito směnami. Pro korektní nalezení případné první resp. poslední směny na přelomu začátku resp. konce úseku určeného vstupními parametry datumOd, datumDo musí proto začít resp. skončit směny procházet dřív resp. později. Tento přesah volíme tak, aby byl dostatečně delší než je délka kterékoliv směny - volíme 3 dny.
  • Procházený úsek nemusí být jen uvnitř jednoho měsíce, ale může být i na přelomu měsíců (*), metoda proto bude nejprve procházet po sobě jsoucí měsíce určené výrazy datumOd.AddDays(-3)datumDo.AddDays(3). To bude zajištěno pomocnou metodou EnumerateMonths.
  • Pro každý měsíc metoda načte pracovní směny používat objektu PracovniCasyOsobaMesic, který umožňuje vrátit směny pro jednotlivé dny v měsíci.
  • Dny se “vyfiltrují”, aby spadali to časového úseku <datumOd.AddDays(-3), datumDo.AddDays(3)).
  • Pro tyto dny se budou procházet všechny definované směny.
  • Protože metoda bude procházet pracovní směny, ale vracet bude intervaly mezi těmito směnami, je potřeba si pamatovat předchozí směnu.
  • První vrácený interval je určený tím, že aktuálně procházená směna má začátek větší než je datumOd.
  • Poslední vracený interval je určený tím, že aktuálně procházená směna má konec menší nebo roven datumDo.
  • Pokud do přelomu začátku resp. konce úseku určeného vstupními parametry datumOd, datumDo směna nezasahuje, je počátek prvního resp. konec posledního vráceného intervalu roven hodnotě datumOd resp. datumDo.
  • Vždy bude vrácen alespoň jeden interval odpovídající celému úseku datumOd, datumDo.
  • Pokud dvě směny na sebe těsně navazují, interval délky 0 se nevrací.

Odpovídající implementace bude:

private static IEnumerable<TydenInterval> EnumerateWeekIntervals(int IDOsoby, DateTime datumOd, DateTime datumDo)
{
    LxPracovniCasyOsobaDenSmena predchoziSmena = null;
    LxPracovniCasyOsobaDenSmena posledniSmena = null;
    TydenInterval interval;

    //Intervaly uvnitř týdne se hledájí od datumOd - 3 do datumDo + 3 (dostatečná rezerva pro nalezení případných směn na přelomu začátku a konce týdne).
    //Pro první a poslední interval se uvažují i okamžiky vlastního začátku a konce týdne, pokud nezasahují přímo do nějaké směny.
    foreach (var smena in from mesicARok in EnumerateMonths(datumOd.AddDays(-3), datumDo.AddDays(3))
                          from den in LxPracovniCasyOsobaMesic.Get(IDOsoby, mesicARok.Year, mesicARok.Month).JednotliveDny
                          where den.Datum >= datumOd.AddDays(-3) && den.Datum < datumDo.AddDays(3)
                          from smena in den.Smeny
                          select smena)
    {
        if (smena.DatumACasOd > datumOd)    //Začátek 
        {
            if (smena.DatumACasDo >= datumDo)   //Nalezena poslední směna
            {
                posledniSmena = smena;
                break;
            }

            interval = new TydenInterval(predchoziSmena == null || predchoziSmena.DatumACasDo < datumOd ? datumOd : predchoziSmena.DatumACasDo, smena.DatumACasOd);
            if (interval.Delka > TimeSpan.Zero) //Dvě hned po sobě jdoucí směny (interval se ignoruje) 
            {
                yield return interval;
            }
        }

        predchoziSmena = smena;
    }

    //Poslední interval
    interval = new TydenInterval(predchoziSmena == null || predchoziSmena.DatumACasDo < datumOd ? datumOd : predchoziSmena.DatumACasDo, posledniSmena == null || posledniSmena.DatumACasOd > datumDo ? datumDo : posledniSmena.DatumACasOd);
    if (interval.Delka > TimeSpan.Zero) //Dvě hned po sobě jdoucí směny (interval se ignoruje) 
    {
        yield return interval;
    }
}

private static IEnumerable<DateTime> EnumerateMonths(DateTime datumOd, DateTime datumDo)
{
    for (DateTime mesicARok = new DateTime(datumOd.Year, datumOd.Month, 1); mesicARok <= datumDo; mesicARok = mesicARok.AddMonths(1))
    {
        yield return mesicARok;
    }
}

Příště náš algoritmus dokončíme.


(*) Obecně by mohl být i delší než jeden měsíc.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback