<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xml" href="/cjs/screen.xsl" media="screen"?>
<lecture>

<meta>
  <maintitle>Python</maintitle>
  <author>Jiří Znamenáček</author>
  <title>Práce s datem a časem</title>
  <date>2018-05-16</date>
  <link><!--a href="http://vyuka.ookami.cz" rel="external">http://vyuka.ookami.cz</a--></link>
  <!--
  <style>
    /* CSSka */
  </style>
  <use-math/>
  -->
</meta>
<!--
  „“–…
  ←→ ↑↓ ↔↕
  ↵ aneb &#x21B5; aneb \r aneb CR aneb CarriageReturn
-->


<slide title="Úvod">

    <p>
        Práce s časem a datumem je v Python'u roztroušena na několika místech, především asi podle svého primárního použití. Část metod tak vypadá duplikovaně a některé věci najdete nejspíše tam, kde byste je nehledali.
    </p>
    <p>
        Tato přednáška se nebude ani náhodou pokoušet o ucelený přehled dostupné funkcionality (které je opravdu mnoho), ale zato se pokusí vypíchnout některé důležité koncepty a pár užitečných nástrojů<sup>*</sup>.
    </p>
    <note>
        <sup>*</sup> Tím je pochopitelně míněno především to, co jsem někdy potřeboval, a když už jsem to zjistil, tak by byla škoda to nezapsat ^_~
    </note>

</slide>
<slide title="Prerekvizity">

    <p>
        K chování časových operací je třeba mít alespoň povšechné povědomí o několika základních konceptech týkajících se měření času a určování data v počítačovém světě:
    </p>
    <ul>
        <li>
            Svět počítačový určuje čas podle počtu sekund uběhlých od tzv. <strong>počátku epochy</strong>, kterým je deklarativně začátek – tedy půlnoc – 1. ledna 1970 gregoriánského kalendáře (a zjevně to byl čtvrtek).
        </li>
        <li>
            Nicméně tento <em>epochový čas</em> si většinou nehraje na dorovnávání nepřesností se skutečností pomocí tzv. <strong>přestupných sekund</strong>, takže se za prvé rozjíždí (protože den skutečně nemá právě 86&#160;400 sekund; v dohledné době pro lidi ne až tak moc viditelný efekt) a za druhé některé systémy <em>přestupné sekundy</em> započítávají.
        </li>
        <li>
            Spousta systémů – především z historické doby – ukládá počet sekund jako 32-bitové celé číslo. Což znamená že 19. ledna 2038 (UTC, plus mínus nějaká ta přestupná sekunda ;-) přetečou.
        </li>
    </ul>
    <p>
        K těmto obecným ještě pár konkrétnějších poznámek:
    </p>
    <ul>
        <li>
            Jelikož sekundová přesnost je pro spoustu aplikací naprosto nedostačující, začalo se časem přecházet na <strong>přesnost nanosekundovou</strong>. Nicméně ne všechny systémy a programovací jazyky jí vyhradily dostatečně přesnou datovou reprezentaci, takže občas sem tam nějaká ta třebas mikrosekunda uletí…
        </li>
        <li>
            Každý jistě z vlastní zkušenosti zná zábavu zvanou <strong>letní čas</strong>, případně též související <strong>časové pásmo</strong>. Jak řešit kupříkladu ukládání logovacích souborů při náhlé změně času o hodinu libovolným směrem nebo zda za uživatele „inteligentně“ předvídat, v jakémže konkrétním čase ta která událost tedy vlastně nastala, rozhodně není žádný med…
        </li>
    </ul>
    <p>
    </p>

</slide>
<slide title="Naivní a informované časové údaje">

    <p>
        Při práci s časem je (kromě požadované přesnosti) zdaleka nejdůležitější vědět:
    </p>
    <blockquote>
        Zda používaná operace bere v potaz časová pásma a letní čas nebo nikoli.
    </blockquote>
    <p>
        A toto skutečně nepodceňujte! Kupříkladu všude citovaná funkce <em>time.mktime()</em> vracející počet sekund od začátku epochy (jako reálné číslo) je za prvé závislá na platformě a za druhé provádí automatický přepočet podle lokálního času (respektive toho, co si daný systém myslí, že je lokální čas, což může být ještě horší).
    </p>
    <p>
        Python sice pochopitelně obsahuje základní informace o časových pásmech a letním čase (v podobě třídy <em>timezone</em>), ale z pochopitelných důvodů nejsou příliš obsáhlé (především nejdou moc mimo epochu a jsou často závislé na konkrétní platformě). <strong>Pro jakoukoliv trochu serióznější práci s časovými pásmy a posuny proto použijte externí modul</strong> <a href="http://pytz.sourceforge.net" class="external"><strong>pytz</strong></a> (<em>World Timezone Definitions for Python</em>).
    </p>

</slide>
<slide title="Naivní a informované časové údaje – „tzinfo“">

    <p>
        Uvedený požadavek na zařazení časových objektů do dvou skupin podle jejich přístupu k (absolutní) interpretaci časového údaje je realizován existencí tzv. <strong>naivních</strong> a <strong>informovaných</strong> časových údajů:
    </p>
    <ul>
        <li>
            <strong>informované</strong> (<em>aware</em>) <strong>časové údaje</strong> mají dostatečnou znalost lokálně (a globálně) aplikovaných pravidel pro přesné umístění události v čase (a to samozřejmě včetně časových zón, letního času a podobných vymyšleností) a tím pádem i relativně vůči ostatním informovaným časovým údajům;
        </li>
        <li>
            <strong>naivní</strong> (<em>naive</em>) <strong>časové údaje</strong> postrádají (dostatečnou) znalost pro své umístění v čase (tedy především relativně vůči ostatním časovým objektům).
        </li>
    </ul>
    <p>
        Zjednodušeně by se dalo říci, že <em>naivní objekty</em> jsou prostě číslo, jehož interpretace je zcela v rukách programátora, zatímco <em>informované objekty</em> představují naprosto jasný údaj o čase, o kterém se nedá diskutovat.
    </p>
    <p>
        Prakticky se tyto objekty dají rozeznat podle hodnoty svého atributu <strong><em>tzinfo</em></strong> – naivní v něm obsahují hodnotu <code>None</code>, zatímco informované referenci na konkrétní instanci abstraktní třídy <em>tzinfo</em> (kterážto je v Python'u tedy pouze jedna – <em>timezone</em> – a pro všechny ostatní se podívejte na již zmiňovaný <a href="http://pytz.sourceforge.net" class="external"><strong>pytz</strong></a>).
    </p>

</slide>
<slide title="Formát zápisu data a času">

    <p>
        Kromě počtu (nano)sekund od počátku epochy se často čas předává pomocí datové struktury <em>struct_time</em>, která drží kompletní informaci o datu včetně dne v týdnu, v roce a (případné) aplikaci letního posunu:
    </p>
    <example lang="python">
        # UTC-čas
        >>> time.gmtime()
        time.struct_time(
            tm_year=2018, tm_mon=6, tm_mday=7, tm_hour=12, tm_min=53, tm_sec=53,
            tm_wday=3, tm_yday=158, tm_isdst=0
        )

        # lokální čas
        >>> time.localtime()
        time.struct_time(
            tm_year=2018, tm_mon=6, tm_mday=7, tm_hour=14, tm_min=54, tm_sec=27,
            tm_wday=3, tm_yday=158, tm_isdst=1
        )
    </example>
    <note>
        Zalámáno pro lepší čitelnost.
    </note>
    <p>
        Některé metody vrací datum/čas právě v této podobě, jiné ho zase jsou schopné v této podobě přijímat.
    </p>

</slide>
<slide title="Parsování data z/do řetězce">

    <p>
        Častou operací je převod data a času z nějakého řetězcového zápisu do počítačové reprezentace a zpět. Python na pro uvedenou funkcionalitu volá (platformně závislou) céčkovskou knihovní funkci <em>strftime()</em>. Příklad:
    </p>
    <example lang="python">
        >>> from datetime import datetime

        # z řetězce na objekt
        >>> datetime.strptime("20180427_171439", '%Y%m%d_%H%M%S')
        datetime.datetime(2018, 4, 27, 17, 14, 39)

        # z objektu na řetězec
        >>> t = datetime.strptime("20180427_171439", '%Y%m%d_%H%M%S')
        >>> t.strftime('%c')    # lokálně závislý plný výpis
        'Fri Apr 27 17:14:39 2018'
        >>> t.strftime('%d. %B %Y, %H:%M:%S, %A, %j. den roku') # vlastní výpis
        '27. April 2018, 17:14:39, Friday, 117. den roku'

    </example>
    <note>
        PS: „Poloviční“ objekty pouze pro datum <em>date</em> nebo pouze pro čas <em>time</em> pochopitelně nadbytečné údaje nepotřebují a pokud je přesto zadáte, nemusí to vždy dopadnout podle vašich představ.
    </note>
    <p>
        Jelikož formátovacích příznaků je opravdu hodně (a to i jenom těch platformně nezávislých), odkazuji pro ně na <a href="https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior" class="external">oficiální dokumentaci</a>.
    </p>

</slide>
<slide title="„replace()“ &amp; „fold“">

    <p>
        Velmi užitečná metoda <em>replace()</em> zamění na vstupním časovém objektu pouze ty části, které dostane jako své parametry:
    </p>
    <blockquote>
        datetime.replace(year=self.year, month=self.month, day=self.day, hour=self.hour, minute=self.minute, second=self.second, microsecond=self.microsecond, tzinfo=self.tzinfo, *, fold=0)
    </blockquote>
    <note>
        Parametr <em>fold</em> slouží (od verze Python'u 3.6) k rozlišení pořadí časových okamžiků, které kvůli skokové změně času (např. přechod na letní čas) ukazují zdánlivě na stejnou hodnotu (0 obdrží ten dřívější, 1 ten pozdější).
    </note>
    <p>
        Několik příkladů:
    </p>
    <example lang="python">
        >>> from datetime import datetime, timezone
        >>> dt = datetime.strptime("20180427_171439", '%Y%m%d_%H%M%S')
        >>> dt
        datetime.datetime(2018, 4, 27, 17, 14, 39)

        # naivní časový objekt
        >>> dt.replace(year=2017)
        datetime.datetime(2017, 4, 27, 17, 14, 39)

        # informovaný časový objekt
        >>> dt.replace(tzinfo=timezone.utc)
        datetime.datetime(2018, 4, 27, 17, 14, 39, tzinfo=datetime.timezone.utc)
    </example>
    <p>
    </p>

</slide>
<slide title="Převod z data na sekundy">

    <p>
        Od Python'u 3.3 je asi nejsnadnějším způsobem, jak získat počet sekund od začátku epochy k danému časovému okamžiku, metoda <strong><em>datetime.datetime.timestamp()</em></strong>:
    </p>
    <example lang="python">
        >>> from datetime import datetime
        >>> datetime.timestamp( datetime.now() )
        1529340048.165769
    </example>
    <p>
        Nicméně i zde je schovaný zádrhel:
    </p>
    <blockquote>
        Pokud je použitý <em>datetime</em> objekt <strong>naivní</strong>, je předpokládán čas zadaný jako lokální a ke konverzi je použita platformní céčkovská funkce <em>mktime()</em>, zatímco pokud je objekt <strong>informovaný</strong>, je výsledná hodnota spočítána jako <code>dt.replace(tzinfo=timezone.utc).timestamp()</code> .
    </blockquote>
    <p>
        Kromě toho, že podle povahy vstupního časového objektu můžete dostat různé výsledky, má také céčkovská funkce <em>mktime()</em> menší rozsah platnosti než odpovídající pythoní objekt, takže někdy můžete při zcela nevinném dotazu obdržet výjimku <em>OverflowError</em>.
    </p>

</slide>
<slide title="Převod ze sekund na datum">

    <p>
        Nepřekvapivě k předchozímu převodu existuje inverzní metoda <strong><em>datetime.fromtimestamp(timestamp, tz=None)</em></strong> provádějící opačnou konverzi
    </p>
    <example lang="python">
        >>> from datetime import datetime, timezone

        >>> dt = datetime.timestamp( datetime.now() )
        >>> dt
        1529409724.25855
        >>> datetime.fromtimestamp(dt)
        datetime.datetime(2018, 6, 19, 14, 2, 4, 258550)
    </example>
    <p>
        A nepřekvapivě poznámky k ní jsou úplně stejné jako k předchozí, tzn. že bez udání časové zóny <em>tzinfo</em> se předpokládá lokální čas a vrácený časový objekt je naivní.
    </p>

</slide>
<slide title="„time.gmtime()“ a „calendar.timegm()“">

    <p>
        Inverzní funkce k funkci <strong><em>time.gmtime([SEKUNDY])</em></strong> se trochu překvapivě schovává v modulu <em>calendar</em> – <strong><em>calendar.timegm([ČasováNtice])</em></strong>:
    </p>
    <example lang="python">
        >>> import time
        >>> import calendar

        >>> dt = time.gmtime(0)
        >>> dt
        time.struct_time(
            tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=0, tm_min=0, tm_sec=0,
            tm_wday=3, tm_yday=1, tm_isdst=0
        )

        >>> calendar.timegm(dt)
        0
    </example>
    <note>
        Zalámáno pro lepší čitelnost.
    </note>

</slide>
<slide title="„os.utime()“">

    <p>
        Tato funkce slouží k nastavení času modifikace a posledního přístupu k zadané cestě<sup>*</sup>.
    </p>
    <note>
        <sup>*</sup> Dejte si pozor, že <em>cesta</em> může podle platformy znamenat různé věci! Kupříkladu na Windows mezi podporované cesty nepatří adresáře (protože nejsou implementovány jako soubory).
    </note>
    <p>
        K základnímu použití patří tři následující způsoby volání:
    </p>
    <ul>
        <li>
            <strong><code>utime(CESTA)</code></strong> – nastaví čas přístupu a modifikace na aktuální;
        </li>
        <li>
            <strong><code>utime(CESTA, times=(atime, mtime))</code></strong> – nastaví parametry souboru podle sekund od začátku epochy (v pořadí poslední přístup – poslední modifikace);
        </li>
        <li>
            <strong><code>utime(CESTA, ns=(atime_ns, mtime_ns))</code></strong> – totéž jako předchozí, ale s přesností na nasekundy.
        </li>
    </ul>
    <p>
        Sekundy a nanosekundy nelze míchat. Jinak se naštěstí metoda chová očekávatelně – co do ní pošlete, to zapíše.
    </p>

</slide>
<slide title="Odkazy">

    <p>
        Jelikož problematika času je neuvěřitelně rozsáhlá, místo závěru odkazy do oficiální dokumentace, až budete (a že budete!) potřebovat více:
    </p>
    <ul>
        <li>
            modul <a href="https://docs.python.org/3/library/datetime.html" class="external"><em>datetime</em></a> (podmoduly <em>datetime</em> plus <em>date</em> a <em>time</em>, <em>tzinfo</em> a <em>timezone</em>, <em>timedelta</em>)
        </li>
        <li>
            modul <a href="https://docs.python.org/3/library/time.html" class="external"><em>time</em></a>
        </li>
        <li>
            modul <a href="https://docs.python.org/3/library/calendar.html" class="external"><em>calendar</em></a>
        </li>
    </ul>

</slide>


</lecture>
