]> git.madduck.net Git - etc/awesome.git/commitdiff

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Add '.config/awesome/modules/luatz/' from commit 'bdbbf89c38126a71b17049469e8f976c571...
authormartin f. krafft <madduck@madduck.net>
Mon, 12 Feb 2018 21:38:41 +0000 (10:38 +1300)
committermartin f. krafft <madduck@madduck.net>
Mon, 12 Feb 2018 21:38:41 +0000 (10:38 +1300)
git-subtree-dir: .config/awesome/modules/luatz
git-subtree-mainline: de5931c65f3f8a7138a412fcdfa8fce830807679
git-subtree-split: bdbbf89c38126a71b17049469e8f976c571b9392

36 files changed:
.config/awesome/modules/luatz/.busted [new file with mode: 0644]
.config/awesome/modules/luatz/.gitignore [new file with mode: 0644]
.config/awesome/modules/luatz/.luacheckrc [new file with mode: 0644]
.config/awesome/modules/luatz/.luacov [new file with mode: 0644]
.config/awesome/modules/luatz/.travis.yml [new file with mode: 0644]
.config/awesome/modules/luatz/COPYING [new file with mode: 0644]
.config/awesome/modules/luatz/NEWS [new file with mode: 0644]
.config/awesome/modules/luatz/README.md [new file with mode: 0644]
.config/awesome/modules/luatz/doc/Makefile [new file with mode: 0644]
.config/awesome/modules/luatz/doc/README.md [new file with mode: 0644]
.config/awesome/modules/luatz/doc/gettime.md [new file with mode: 0644]
.config/awesome/modules/luatz/doc/index.md [new file with mode: 0644]
.config/awesome/modules/luatz/doc/links.md [new file with mode: 0644]
.config/awesome/modules/luatz/doc/metadata.yaml [new file with mode: 0644]
.config/awesome/modules/luatz/doc/parse.md [new file with mode: 0644]
.config/awesome/modules/luatz/doc/site.css [new file with mode: 0644]
.config/awesome/modules/luatz/doc/template.html [new file with mode: 0644]
.config/awesome/modules/luatz/doc/timetable.md [new file with mode: 0644]
.config/awesome/modules/luatz/doc/tzinfo.md [new file with mode: 0644]
.config/awesome/modules/luatz/examples/date_arithmetic.lua [new file with mode: 0644]
.config/awesome/modules/luatz/examples/os_date.lua [new file with mode: 0644]
.config/awesome/modules/luatz/luatz-scm-0.rockspec [new file with mode: 0644]
.config/awesome/modules/luatz/luatz/gettime.lua [new file with mode: 0644]
.config/awesome/modules/luatz/luatz/init.lua [new file with mode: 0644]
.config/awesome/modules/luatz/luatz/parse.lua [new file with mode: 0644]
.config/awesome/modules/luatz/luatz/strftime.lua [new file with mode: 0644]
.config/awesome/modules/luatz/luatz/timetable.lua [new file with mode: 0644]
.config/awesome/modules/luatz/luatz/tzcache.lua [new file with mode: 0644]
.config/awesome/modules/luatz/luatz/tzfile.lua [new file with mode: 0644]
.config/awesome/modules/luatz/luatz/tzinfo.lua [new file with mode: 0644]
.config/awesome/modules/luatz/spec/Godthab.tz [new file with mode: 0644]
.config/awesome/modules/luatz/spec/parse_spec.lua [new file with mode: 0644]
.config/awesome/modules/luatz/spec/strftime_spec.lua [new file with mode: 0644]
.config/awesome/modules/luatz/spec/timetable_spec.lua [new file with mode: 0644]
.config/awesome/modules/luatz/spec/tzcache_spec.lua [new file with mode: 0644]
.config/awesome/modules/luatz/spec/tzfile_spec.lua [new file with mode: 0644]

diff --git a/.config/awesome/modules/luatz/.busted b/.config/awesome/modules/luatz/.busted
new file mode 100644 (file)
index 0000000..d1a4ad9
--- /dev/null
@@ -0,0 +1,5 @@
+return {
+    default = {
+        lpath = "./?.lua";
+    };
+}
diff --git a/.config/awesome/modules/luatz/.gitignore b/.config/awesome/modules/luatz/.gitignore
new file mode 100644 (file)
index 0000000..a299fa5
--- /dev/null
@@ -0,0 +1,4 @@
+luatz-*.rock
+doc/luatz.3
+doc/luatz.html
+doc/luatz.pdf
diff --git a/.config/awesome/modules/luatz/.luacheckrc b/.config/awesome/modules/luatz/.luacheckrc
new file mode 100644 (file)
index 0000000..b1f4e2e
--- /dev/null
@@ -0,0 +1,2 @@
+std = "min"
+files["spec"] = {std = "+busted"}
diff --git a/.config/awesome/modules/luatz/.luacov b/.config/awesome/modules/luatz/.luacov
new file mode 100644 (file)
index 0000000..9e2d541
--- /dev/null
@@ -0,0 +1,10 @@
+return {
+       statsfile = "luacov.stats.out";
+       reportfile = "luacov.report.out";
+       deletestats = true;
+       include = {
+               "^./luatz/";
+       };
+       exclude = {
+       };
+}
diff --git a/.config/awesome/modules/luatz/.travis.yml b/.config/awesome/modules/luatz/.travis.yml
new file mode 100644 (file)
index 0000000..7015a29
--- /dev/null
@@ -0,0 +1,43 @@
+language: python
+
+sudo: false
+
+env:
+  matrix:
+    - LUA="lua 5.1" SOCKET=true
+    - LUA="lua 5.1"
+    - LUA="lua 5.2" SOCKET=true
+    - LUA="lua 5.2"
+    - LUA="lua 5.3" SOCKET=true
+    - LUA="lua 5.3"
+    - LUA="luajit 2.0"
+    - LUA="luajit 2.1" SOCKET=true SYSCALL=true
+    - LUA="luajit 2.1" SYSCALL=true
+    - LUA="luajit 2.1"
+    - LUA="luajit @"
+
+before_install:
+  - pip install hererocks
+  - hererocks here -r^ --$LUA # Install latest LuaRocks version
+                              # plus the Lua version for this build job
+                              # into 'here' subdirectory
+  - export PATH=$PATH:$PWD/here/bin # Add directory with all installed binaries to PATH
+  - eval `luarocks path --bin`
+  - luarocks install luacov-coveralls
+  - luarocks install busted
+
+install:
+  - luarocks make
+  - if [ "$SOCKET" = "true" ]; then luarocks install luasocket; fi
+  - if [ "$SYSCALL" = "true" ]; then luarocks install ljsyscall; fi
+
+script:
+  - busted -c
+
+after_success:
+  - luacov-coveralls -v
+
+notifications:
+  email:
+    on_success: change
+    on_failure: always
diff --git a/.config/awesome/modules/luatz/COPYING b/.config/awesome/modules/luatz/COPYING
new file mode 100644 (file)
index 0000000..b4435bb
--- /dev/null
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2013-2017 Daurnimator
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/.config/awesome/modules/luatz/NEWS b/.config/awesome/modules/luatz/NEWS
new file mode 100644 (file)
index 0000000..851fc16
--- /dev/null
@@ -0,0 +1,31 @@
+UNRELEASED
+
+
+0.4 - 2017-12-09
+
+  - Fix timetable normalisation carry bugs (#10, #13)
+  - Clean up of docs
+  - No longer throw errors in parse module on error (now return nil, err)
+  - Support version 3 tzfiles
+
+
+0.3 - 2015-01-02
+
+  - Lua 5.3 support
+  - Fix bug in rfc-3339 serialisation (#4)
+
+
+0.2 - 2014-08-29
+
+  - Support for fractional timetable component normalisation
+    e.g. .month=6.5, .day=1 (which could be read as "the first day after the middle of June") normalises to .month=2, .day=16
+  - Top level aliases for common operations
+  - Own implementation of stftime formatting (locales are not yet complete)
+  - Uses ljsyscall for more accurate time when available
+
+
+0.1 - 2013-11-23
+
+  - provides a `os.date` compatible class "timetable"
+  - timezone conversion
+  - rfc3339 parsing
diff --git a/.config/awesome/modules/luatz/README.md b/.config/awesome/modules/luatz/README.md
new file mode 100644 (file)
index 0000000..0c4575c
--- /dev/null
@@ -0,0 +1,29 @@
+# luatz
+
+A lua library for time and date manipulation.
+
+Features include:
+
+  - Normalisation of broken down date objects
+         - allows for complex time/date manipulation logic e.g. "What day is it in 2 days, 5 hours from now?"
+  - Conversion between locations (time zones) using your local [zoneinfo](https://www.iana.org/time-zones) database.
+  - `strftime` style formatting
+
+
+[![Build Status](https://travis-ci.org/daurnimator/luatz.png)](https://travis-ci.org/daurnimator/luatz) [![Coverage Status](https://coveralls.io/repos/github/daurnimator/luatz/badge.svg?branch=master)](https://coveralls.io/github/daurnimator/luatz?branch=master)
+
+Supported under Lua 5.1, 5.2, 5.3 and LuaJIT.
+
+
+## Documentation
+
+Documentation can be found in the `doc` sub-directory.
+
+An online version can be found at https://daurnimator.github.io/luatz/
+
+
+## Installation
+
+### via [luarocks](https://luarocks.org/modules/daurnimator/luatz)
+
+    luarocks install luatz
diff --git a/.config/awesome/modules/luatz/doc/Makefile b/.config/awesome/modules/luatz/doc/Makefile
new file mode 100644 (file)
index 0000000..7d30015
--- /dev/null
@@ -0,0 +1,26 @@
+FILES = \
+       index.md \
+       gettime.md \
+       parse.md \
+       timetable.md \
+       tzinfo.md \
+       links.md
+
+all: luatz.html luatz.pdf luatz.3
+
+luatz.html: template.html site.css metadata.yaml $(FILES)
+       pandoc -o $@ -t html5 -s --toc --template=template.html --section-divs --self-contained -c site.css metadata.yaml $(FILES)
+
+luatz.pdf: metadata.yaml $(FILES)
+       pandoc -o $@ -t latex -s --toc --toc-depth=2 -V documentclass=article -V classoption=oneside -V links-as-notes -V geometry=a4paper,includeheadfoot,margin=2.54cm metadata.yaml $(FILES)
+
+luatz.3: metadata.yaml $(FILES)
+       pandoc -o $@ -t man -s metadata.yaml $(FILES)
+
+man: luatz.3
+       man -l $^
+
+clean:
+       rm -f luatz.html luatz.pdf luatz.3
+
+.PHONY: all man install clean
diff --git a/.config/awesome/modules/luatz/doc/README.md b/.config/awesome/modules/luatz/doc/README.md
new file mode 100644 (file)
index 0000000..ed82567
--- /dev/null
@@ -0,0 +1,5 @@
+Documentation in this directory is intended to be converted to other formats using [pandoc](http://pandoc.org/).
+
+An online HTML version can be found at [https://daurnimator.github.io/luatz/](https://daurnimator.github.io/luatz/)
+
+The *Makefile* in this directory should be used to compile the documentation.
diff --git a/.config/awesome/modules/luatz/doc/gettime.md b/.config/awesome/modules/luatz/doc/gettime.md
new file mode 100644 (file)
index 0000000..3550577
--- /dev/null
@@ -0,0 +1,27 @@
+## `luatz.gettime` <!-- --> {#gettime}
+
+A module to get the current time.
+
+Uses the most precise method available (in order:)
+
+  - Use [ljsyscall](http://www.myriabit.com/ljsyscall/) to access `clock_gettime(2)` called with `CLOCK_REALTIME`
+  - [lunix](http://25thandclement.com/~william/projects/lunix.html)'s `unix.clock_gettime()` (Only on non-Apple systems)
+  - Use [ljsyscall](http://www.myriabit.com/ljsyscall/) to access `gettimeofday(2)`
+  - [lunix](http://25thandclement.com/~william/projects/lunix.html)'s `unix.gettimeofday()`
+  - [luasocket](http://w3.impa.br/~diego/software/luasocket/)'s `socket.gettime`
+  - [Openresty](http://openresty.org/)'s [`ngx.now`](http://wiki.nginx.org/HttpLuaModule#ngx.now)
+  - [`os.time`](http://www.lua.org/manual/5.3/manual.html#pdf-os.time)
+
+### `source` <!-- --> {#gettime.source}
+
+The library/function currently in use by [`gettime()`](#gettime.gettime).
+
+
+### `resolution` <!-- --> {#gettime.resolution}
+
+The smallest time resolution (in seconds) available from [`gettime()`](#gettime.gettime).
+
+
+### `gettime()` <!-- --> {#gettime.gettime}
+
+Returns the number of seconds since unix epoch (1970-01-01T00:00:00Z) as a lua number
diff --git a/.config/awesome/modules/luatz/doc/index.md b/.config/awesome/modules/luatz/doc/index.md
new file mode 100644 (file)
index 0000000..b69b2d8
--- /dev/null
@@ -0,0 +1,51 @@
+## `luatz`
+
+Requiring the base luatz module will give you a table of commonly used functions and submodules.
+
+The table includes the following sub modules, which have their own documentation:
+
+  - [`parse`](#parse): Parses common date/time formats
+  - [`timetable`](#timetable): Class for date/time objects supporting normalisation
+
+### `time()` <!-- --> {#luatz.time}
+
+Returns the current unix timestamp using the most precise source available.
+See [`gettime`](#gettime) for more information.
+
+
+### `now()` <!-- --> {#luatz.now}
+
+Returns the current time as a timetable object
+See `timetable` for more information
+
+
+### `get_tz([timezone_name])` <!-- --> {#luatz.get_tz}
+
+Returns a timezone object (see `tzinfo` documentation) for the given `timezone_name`.
+If `timezone_name` is `nil` then the local timezone is used.
+If `timezone_name` is an absolute path, then that `tzinfo` file is used
+
+This uses the local [zoneinfo database](https://www.iana.org/time-zones); 
+names are usually of the form `Country/Largest_City` e.g. "America/New_York".
+Check [wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for an example list.
+
+
+### `time_in(timezone_name[, utc_ts])` <!-- --> {#luatz.time_in}
+
+Returns the current time in seconds since 1970-01-01 0:00:00 in the given timezone as a string,
+(same semantics as [`get_tz`](#luatz.get_tz)) at the given UTC time (defaults to now).
+
+
+### `gmtime(ts)` <!-- --> {#luatz.gmtime}
+
+As in the C standard library
+
+
+### `localtime(ts)` <!-- --> {#luatz.localtime}
+
+As in the C standard library
+
+
+### `ctime(ts)` <!-- --> {#luatz.ctime}
+
+As in the C standard library
diff --git a/.config/awesome/modules/luatz/doc/links.md b/.config/awesome/modules/luatz/doc/links.md
new file mode 100644 (file)
index 0000000..3e8905f
--- /dev/null
@@ -0,0 +1,5 @@
+# Links
+
+  - [Github](https://github.com/daurnimator/luatz)
+  - [Issue tracker](https://github.com/daurnimator/luatz/issues)
+  - [luarocks](https://luarocks.org/modules/daurnimator/luatz)
diff --git a/.config/awesome/modules/luatz/doc/metadata.yaml b/.config/awesome/modules/luatz/doc/metadata.yaml
new file mode 100644 (file)
index 0000000..4f144f0
--- /dev/null
@@ -0,0 +1,6 @@
+---
+title: luatz
+subtitle: A lua library for time and date manipulation
+author: Daurnimator <quae@daurnimator.com>
+section: 3
+...
diff --git a/.config/awesome/modules/luatz/doc/parse.md b/.config/awesome/modules/luatz/doc/parse.md
new file mode 100644 (file)
index 0000000..c41eae5
--- /dev/null
@@ -0,0 +1,12 @@
+## `luatz.parse` <!-- --> {#parse}
+
+Provides parsers for common time and date formats.
+
+Functions take the source string and an optional initial postition.
+
+### `rfc_3339(string[, init])` <!-- --> {#parse.rfc_3339}
+
+If the string is a valid RFC-3339 timestamp,
+returns a luatz timetable and the (optional) time zone offset in seconds.
+
+Otherwise returns `nil` and an error message
diff --git a/.config/awesome/modules/luatz/doc/site.css b/.config/awesome/modules/luatz/doc/site.css
new file mode 100644 (file)
index 0000000..8858002
--- /dev/null
@@ -0,0 +1,156 @@
+* {
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box
+}
+html,
+body {
+    height: 100%
+}
+article,
+aside,
+figure,
+footer,
+header,
+hgroup,
+menu,
+nav,
+section {
+    display: block
+}
+body {
+    margin: 0
+}
+h1,
+h2,
+h3 {
+    margin: 1rem 0
+}
+h4,
+h5,
+h6,
+ul,
+ol,
+dl,
+blockquote,
+address,
+p,
+figure {
+    margin: 0 0 1rem 0
+}
+img {
+    max-width: 100%
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+    font-weight: 700
+}
+h1 {
+    font-size: 2.5rem;
+    line-height: 3rem
+}
+h2 {
+    font-size: 1.5rem;
+    line-height: 2rem
+}
+h3 {
+    font-size: 1.25rem;
+    line-height: 1.5rem
+}
+h4,
+h5,
+h6 {
+    font-size: 1rem;
+    line-height: 1.25rem
+}
+hr {
+    border: 0;
+    border-bottom: 1px solid;
+    margin-top: -1px;
+    margin-bottom: 1rem
+}
+a:hover {
+    color: inherit
+}
+small {
+    font-size: .875rem
+}
+ul,
+ol {
+    padding-left: 1rem
+}
+ul ul,
+ul ol,
+ol ol,
+ol ul {
+    margin: 0
+}
+dt {
+    font-weight: 700
+}
+dd {
+    margin: 0
+}
+blockquote {
+    border-left: 1px solid;
+    padding-left: 1rem
+}
+address {
+    font-style: normal
+}
+html {
+    color: #333;
+    font: 100%/1.5  Avenir, 'Helvetica Neue', Helvetica, Arial, sans-serif;
+    -webkit-font-smoothing: antialiased;
+    -webkit-text-size-adjust: 100%;
+        -ms-text-size-adjust: 100%;
+    background: #FFF;
+}
+a {
+    color: #999;
+    text-decoration: none;
+    transition: color 0.3s;
+}
+a > h1,
+a > h2,
+a > h3 {
+    color: #333;
+}
+
+body > * {
+    padding: 0 1rem;
+}
+.subtitle {
+    font-size: 1rem;
+    line-height: 1.5rem
+}
+.author {
+    display: none
+}
+@media screen and (min-width: 55rem) {
+    .meta {
+        position: fixed;
+        width: 20rem;
+        height: 100%;
+        overflow: auto;
+        background: #FFF;
+        z-index: 1;
+    }
+    main {
+        display: block; /* required for e.g. konqueror */
+        margin-left: 20rem;
+        overflow: auto;
+    }
+}
+@media print {
+    section.level1 {
+        page-break-inside: avoid
+    }
+    nav a::after {
+        content: leader('.') target-counter(attr(href url), page, decimal)
+    }
+}
diff --git a/.config/awesome/modules/luatz/doc/template.html b/.config/awesome/modules/luatz/doc/template.html
new file mode 100644 (file)
index 0000000..a74a7b6
--- /dev/null
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html$if(lang)$ lang="$lang$"$endif$$if(dir)$ dir="$dir$"$endif$>
+<head>
+  <meta charset="utf-8">
+  <meta name="generator" content="pandoc">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+$for(author-meta)$
+  <meta name="author" content="$author-meta$">
+$endfor$
+$if(date-meta)$
+  <meta name="dcterms.date" content="$date-meta$">
+$endif$
+$if(keywords)$
+  <meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$">
+$endif$
+  <title>$if(title-prefix)$$title-prefix$ – $endif$$pagetitle$</title>
+  <style type="text/css">code{white-space: pre;}</style>
+$if(quotes)$
+  <style type="text/css">q { quotes: "“" "”" "‘" "’"; }</style>
+$endif$
+$if(highlighting-css)$
+  <style type="text/css">
+$highlighting-css$
+  </style>
+$endif$
+$for(css)$
+  <link rel="stylesheet" href="$css$">
+$endfor$
+$if(math)$
+  $math$
+$endif$
+  <!--[if lt IE 9]>
+    <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
+  <![endif]-->
+$for(header-includes)$
+  $header-includes$
+$endfor$
+</head>
+<body>
+$for(include-before)$
+$include-before$
+$endfor$
+<div class="meta">
+$if(title)$
+<header>
+<h1 class="title">$title$</h1>
+$if(subtitle)$
+<h1 class="subtitle">$subtitle$</h1>
+$endif$
+$for(author)$
+<h2 class="author">$author$</h2>
+$endfor$
+$if(date)$
+<h3 class="date">$date$</h3>
+$endif$
+</header>
+$endif$
+$if(toc)$
+<nav id="$idprefix$TOC">
+$toc$
+</nav>
+$endif$
+</div>
+<main>
+$body$
+</main>
+$for(include-after)$
+$include-after$
+$endfor$
+</body>
+</html>
diff --git a/.config/awesome/modules/luatz/doc/timetable.md b/.config/awesome/modules/luatz/doc/timetable.md
new file mode 100644 (file)
index 0000000..812da43
--- /dev/null
@@ -0,0 +1,71 @@
+## `luatz.timetable` <!-- --> {#timetable}
+
+Provides an class to represent a time and date.
+Objects have no concept of timezone or utc offset.
+
+The fields are intentionally compatible with the lua standard library's `os.date` and `os.time`. Objects have fields:
+
+  - `year`
+  - `month`
+  - `day`
+  - `hour`
+  - `min`
+  - `sec`
+  - `yday` (optional)
+  - `wday` (optional)
+  
+timetable components may be outside of their standard range (e.g. a month component of 
+14) to facilitate arithmetic operations on date components. `:normalise()` can be 
+called to modify components to return to their standard range.
+
+Equality and comparisons should work between timetable objects.
+
+
+### `new(year, month, day, hour, min, sec[, yday[, [wday]])` <!-- --> {#timetable.new}
+
+Returns a new timetable with the given contents.
+
+
+### `new_from_timestamp(timestamp)` <!-- --> {#timetable.new_from_timestamp}
+
+Returns a new (normalised) timetable, given a timestamp in seconds since the unix epoch of 
+1970-01-01.
+
+
+### `timetable:clone()` <!-- --> {#timetable:clone}
+
+Returns a new independent instance of an existing timetable object.
+
+
+### `timetable:normalise()` <!-- --> {#timetable:normalise}
+
+Mutates the current object's time and date components so that are integers within 'normal'
+ranges e.g. `month` is `1`-`12`; `min` is `0`-`59`
+
+First, fractional parts are propagated down.  
+e.g. `.month=6.5` `.day=1` (which could be read as "the first day after the middle of June")
+normalises to `.month=2` `.day=16`
+
+Second, any fields outside of their normal ranges are propagated up  
+e.g. `.hour=10` `.min=100` (100 minutes past 10am)
+normalises to `.hour=11` `.min=40`
+
+
+### `timetable:rfc_3339()` <!-- --> {#timetable:rfc_3339}
+
+Returns the timetable formatted as an rfc-3339 style string.
+The timezone offset (or Z) is not appended.
+The ranges of components are not checked, if you want a valid timestamp,
+[`:normalise()`](#timetable:normalise) should be called first.
+
+This function is also the `__tostring` metamethod for timetable objects
+
+
+### `timetable:timestamp()` <!-- --> {#timetable:timestamp}
+
+Returns the timetable as the number of seconds since unix epoch (1970-01-01) as a lua number.
+
+
+### `timetable:unpack()` <!-- --> {#timetable:unpack}
+
+Unpacks the timetable object; returns `year`, `month`, `day`, `hour`, `min`, `sec`, `yday`, `wday`
diff --git a/.config/awesome/modules/luatz/doc/tzinfo.md b/.config/awesome/modules/luatz/doc/tzinfo.md
new file mode 100644 (file)
index 0000000..d4791e2
--- /dev/null
@@ -0,0 +1,45 @@
+## `luatz.tzinfo` <!-- --> {#tzinfo}
+
+Provides a metatable for the timezone class.
+
+Created in `luatz.tzfile` and managed by `luatz.tzcache`;
+a timezone object contains information about a timezone.
+These objects are based on the information available in a "zoneinfo" file.
+
+Timezone objects should be considered opaque and immutable;
+so the following details can be skipped over.
+
+------------------------------------------------------------------------------
+
+The table contains a sequence of tables that describe the timezone at a given point
+using a `transition_time`: the unix timestamp (in UTC) that this definition starts, and
+a `tt_info` object.
+
+A `tt_info` object contains information about a time offset;
+and contains the following fields:
+
+  - `gmtoff` (number) The offset from GMT (UTC) in seconds
+  - `isdst` (boolean): If this change was declared as daylight savings
+  - `abbrind` (number, abbreviation id)
+  - `abbr` (string): short name for this gmt offset
+  - `isstd` (boolean)
+  - `isgmt` (boolean)
+
+
+### `tzinfo:find_current(utc_ts)` <!-- --> {#tzinfo:find_current}
+
+Returns the relevant `tt_info` object for the given UTC timestamp in the timezone.
+
+
+### `tzinfo:localise(utc_ts)` and `tzinfo:localize(utc_ts)` <!-- --> {#tzinfo:localise}
+
+Convert the given UTC timestamp to the timezone.
+Returns the number of seconds since unix epoch in the given timezone.
+
+
+### `tzinfo:utctime(local_ts)` <!-- --> {#tzinfo:utctime}
+
+Convert the given local timestamp (seconds since unix epoch in the time zone) to a UTC timestamp.
+This may result in ambigous results, in which case multiple values are returned.
+
+e.g. consider that when daylight savings rewinds your local clock from 3am to 2am there will be two 2:30ams.
diff --git a/.config/awesome/modules/luatz/examples/date_arithmetic.lua b/.config/awesome/modules/luatz/examples/date_arithmetic.lua
new file mode 100644 (file)
index 0000000..4065122
--- /dev/null
@@ -0,0 +1,33 @@
+local luatz = require "luatz"
+
+-- We do this a few times ==> Convert a timestamp to timetable and normalise
+local function ts2tt(ts)
+       return luatz.timetable.new_from_timestamp(ts)
+end
+
+-- Get the current time in UTC
+local utcnow = luatz.time()
+local now = ts2tt(utcnow)
+print(now, "now (UTC)")
+
+-- Get a new time object 6 months from now
+local x = now:clone()
+x.month = x.month + 6
+x:normalise()
+print(x, "6 months from now")
+
+-- Find out what time it is in Melbourne at the moment
+local melbourne = luatz.get_tz("Australia/Melbourne")
+local now_in_melbourne = ts2tt(melbourne:localise(utcnow))
+print(now_in_melbourne, "Melbourne")
+
+-- Six months from now in melbourne (so month is incremented; but still the same time)
+local m = now_in_melbourne:clone()
+m.month = m.month + 6
+m:normalise()
+print(m, "6 months from now in melbourne")
+
+-- Convert time back to utc; a daylight savings transition may have taken place!
+-- There may be 2 results, but for we'll ignore the second possibility
+local c, _ = melbourne:utctime(m:timestamp())
+print(ts2tt(c), "6 months from now in melbourne converted to utc")
diff --git a/.config/awesome/modules/luatz/examples/os_date.lua b/.config/awesome/modules/luatz/examples/os_date.lua
new file mode 100644 (file)
index 0000000..ad254ce
--- /dev/null
@@ -0,0 +1,25 @@
+--[[
+Re-implementation of `os.date` from the standard lua library
+]]
+
+local gettime = require "luatz.gettime".gettime
+local new_from_timestamp = require "luatz.timetable".new_from_timestamp
+local get_tz = require "luatz.tzcache".get_tz
+
+local function os_date(format_string, timestamp)
+       format_string = format_string or "%c"
+       timestamp = timestamp or gettime()
+       if format_string:sub(1, 1) == "!" then -- UTC
+               format_string = format_string:sub(2)
+       else -- Localtime
+               timestamp = get_tz():localise(timestamp)
+       end
+       local tt = new_from_timestamp(timestamp)
+       if format_string == "*t" then
+               return tt
+       else
+               return tt:strftime(format_string)
+       end
+end
+
+return os_date
diff --git a/.config/awesome/modules/luatz/luatz-scm-0.rockspec b/.config/awesome/modules/luatz/luatz-scm-0.rockspec
new file mode 100644 (file)
index 0000000..2ceae4c
--- /dev/null
@@ -0,0 +1,40 @@
+package = "luatz"
+version = "scm-0"
+
+description = {
+       summary = "library for time and date manipulation.";
+       detailed = [[
+       A lua library for time and date manipulation.
+
+       Features include:
+         - Normalisation of broken down date objects
+           - allows for complex time/date manipulation logic e.g. "what day is it in 2 days, 5 hours from now?"
+         - Conversion between locations (time zones) using your local zoneinfo database.
+         - strftime style formatting
+
+       All operations are possible without C extensions, though if available they may be used to increase accuracy.
+       ]];
+       license = "MIT";
+}
+
+dependencies = {
+       "lua >= 5.1";
+}
+
+source = {
+       url = "git://github.com/daurnimator/luatz.git";
+}
+
+build = {
+       type = "builtin";
+       modules = {
+               ["luatz.init"]      = "luatz/init.lua";
+               ["luatz.gettime"]   = "luatz/gettime.lua";
+               ["luatz.parse"]     = "luatz/parse.lua";
+               ["luatz.timetable"] = "luatz/timetable.lua";
+               ["luatz.strftime"]  = "luatz/strftime.lua";
+               ["luatz.tzcache"]   = "luatz/tzcache.lua";
+               ["luatz.tzfile"]    = "luatz/tzfile.lua";
+               ["luatz.tzinfo"]    = "luatz/tzinfo.lua";
+       };
+}
diff --git a/.config/awesome/modules/luatz/luatz/gettime.lua b/.config/awesome/modules/luatz/luatz/gettime.lua
new file mode 100644 (file)
index 0000000..4d6f45a
--- /dev/null
@@ -0,0 +1,53 @@
+local _M = {}
+
+_M.source, _M.resolution, _M.gettime = (function()
+       local has_syscall, syscall = pcall(require, "syscall")
+       if has_syscall and syscall.clock_gettime and syscall.c.CLOCK then
+               local clock_id = syscall.c.CLOCK.REALTIME
+               local function timespec_to_number(timespec)
+                       return tonumber(timespec.tv_sec) + tonumber(timespec.tv_nsec) * 1e-9
+               end
+               return "syscall.clock_gettime(CLOCK_REALTIME)",
+                       syscall.clock_getres and timespec_to_number(syscall.clock_getres(clock_id)) or 1e-9,
+                       function()
+                               return timespec_to_number(syscall.clock_gettime(clock_id))
+                       end
+       end
+
+       local has_unix, unix = pcall(require, "unix")
+       -- On Apple devices lunix only uses gettimeofday()
+       if has_unix and unix.clock_gettime and unix.uname and unix.uname().sysname ~= "Darwin" then
+               return "unix.clock_gettime(CLOCK_REALTIME)", 1e-9, function()
+                       return unix.clock_gettime()
+               end
+       end
+
+       if has_syscall and syscall.gettimeofday then
+               local function timeval_to_number(timeval)
+                       return tonumber(timeval.tv_sec) + tonumber(timeval.tv_nsec) * 1e-6
+               end
+               return "syscall.gettimeofday()", 1e-6,
+                       function()
+                               return timeval_to_number(syscall.gettimeofday())
+                       end
+       end
+
+       if has_unix and unix.gettimeofday then
+               return "unix.gettimeofday()", 1e-6, unix.gettimeofday
+       end
+
+       local has_socket, socket = pcall(require, "socket")
+       if has_socket and socket.gettime then
+               -- on windows, this uses GetSystemTimeAsFileTime, which has resolution of 1e-7
+               -- on linux, this uses gettimeofday, which has resolution of 1e-6
+               return "socket.gettime()", 1e-6, socket.gettime
+       end
+
+       if ngx and ngx.now then -- luacheck: ignore 113
+               return "ngx.now()", 1e-3, ngx.now -- luacheck: ignore 113
+       end
+
+       return "os.time()", 1, os.time
+end)()
+
+return _M
diff --git a/.config/awesome/modules/luatz/luatz/init.lua b/.config/awesome/modules/luatz/luatz/init.lua
new file mode 100644 (file)
index 0000000..4423189
--- /dev/null
@@ -0,0 +1,40 @@
+local _M = {
+       gettime = require "luatz.gettime";
+       parse = require "luatz.parse";
+       strftime = require "luatz.strftime";
+       timetable = require "luatz.timetable";
+       tzcache = require "luatz.tzcache";
+}
+
+--- Top-level aliases for common functions
+
+_M.time = _M.gettime.gettime
+_M.get_tz = _M.tzcache.get_tz
+
+--- Handy functions
+
+_M.time_in = function(tz, now)
+       return _M.get_tz(tz):localize(now)
+end
+
+_M.now = function()
+       local ts = _M.gettime.gettime()
+       return _M.timetable.new_from_timestamp(ts)
+end
+
+--- C-like functions
+
+_M.gmtime = function(ts)
+       return _M.timetable.new_from_timestamp(ts)
+end
+
+_M.localtime = function(ts)
+       ts = _M.time_in(nil, ts)
+       return _M.gmtime(ts)
+end
+
+_M.ctime = function(ts)
+       return _M.strftime.asctime(_M.localtime(ts))
+end
+
+return _M
diff --git a/.config/awesome/modules/luatz/luatz/parse.lua b/.config/awesome/modules/luatz/luatz/parse.lua
new file mode 100644 (file)
index 0000000..6feeb29
--- /dev/null
@@ -0,0 +1,40 @@
+local new_timetable = require "luatz.timetable".new
+
+--- Parse an RFC 3339 datetime at the given position
+-- Returns a time table and the `tz_offset`
+-- Return value is not normalised (this preserves a leap second)
+-- If the timestamp is only partial (i.e. missing "Z" or time offset) then `tz_offset` will be nil
+-- TODO: Validate components are within their boundarys (e.g. 1 <= month <= 12)
+local function rfc_3339(str, init)
+       local year, month, day, hour, min, sec, patt_end = str:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)[Tt](%d%d%.?%d*):(%d%d):(%d%d)()", init) -- luacheck: ignore 631
+       if not year then
+               return nil, "Invalid RFC 3339 timestamp"
+       end
+       year  = tonumber(year, 10)
+       month = tonumber(month, 10)
+       day   = tonumber(day, 10)
+       hour  = tonumber(hour, 10)
+       min   = tonumber(min, 10)
+       sec   = tonumber(sec, 10)
+
+       local tt = new_timetable(year, month, day, hour, min, sec)
+
+       local tz_offset
+       if str:match("^[Zz]", patt_end) then
+               tz_offset = 0
+       else
+               local hour_offset, min_offset = str:match("^([+-]%d%d):(%d%d)", patt_end)
+               if hour_offset then
+                       tz_offset = tonumber(hour_offset, 10) * 3600 + tonumber(min_offset, 10) * 60
+               else -- luacheck: ignore 542
+                       -- Invalid RFC 3339 timestamp offset (should be Z or (+/-)hour:min)
+                       -- tz_offset will be nil
+               end
+       end
+
+       return tt, tz_offset
+end
+
+return {
+       rfc_3339 = rfc_3339;
+}
diff --git a/.config/awesome/modules/luatz/luatz/strftime.lua b/.config/awesome/modules/luatz/luatz/strftime.lua
new file mode 100644 (file)
index 0000000..223da0f
--- /dev/null
@@ -0,0 +1,210 @@
+local strformat = string.format
+local floor = math.floor
+local function idiv(n, d)
+       return floor(n / d)
+end
+
+local c_locale = {
+       abday = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
+       day = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
+       abmon = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
+       mon = {"January", "February", "March", "April", "May", "June",
+               "July", "August", "September", "October", "November", "December"};
+       am_pm = {"AM", "PM"};
+}
+
+--- ISO-8601 week logic
+-- ISO 8601 weekday as number with Monday as 1 (1-7)
+local function iso_8601_weekday(wday)
+       if wday == 1 then
+               return 7
+       else
+               return wday - 1
+       end
+end
+local iso_8601_week do
+       -- Years that have 53 weeks according to ISO-8601
+       local long_years = {}
+       for _, v in ipairs {
+                 4,   9,  15,  20,  26,  32,  37,  43,  48,  54,  60,  65,  71,  76,  82,
+                88,  93,  99, 105, 111, 116, 122, 128, 133, 139, 144, 150, 156, 161, 167,
+               172, 178, 184, 189, 195, 201, 207, 212, 218, 224, 229, 235, 240, 246, 252,
+               257, 263, 268, 274, 280, 285, 291, 296, 303, 308, 314, 320, 325, 331, 336,
+               342, 348, 353, 359, 364, 370, 376, 381, 387, 392, 398
+       } do
+               long_years[v] = true
+       end
+       local function is_long_year(year)
+               return long_years[year % 400]
+       end
+       function iso_8601_week(self)
+               local wday = iso_8601_weekday(self.wday)
+               local n = self.yday - wday
+               local year = self.year
+               if n < -3 then
+                       year = year - 1
+                       if is_long_year(year) then
+                               return year, 53, wday
+                       else
+                               return year, 52, wday
+                       end
+               elseif n >= 361 and not is_long_year(year) then
+                       return year + 1, 1, wday
+               else
+                       return year, idiv(n + 10, 7), wday
+               end
+       end
+end
+
+--- Specifiers
+local t = {}
+function t:a(locale)
+       return "%s", locale.abday[self.wday]
+end
+function t:A(locale)
+       return "%s", locale.day[self.wday]
+end
+function t:b(locale)
+       return "%s", locale.abmon[self.month]
+end
+function t:B(locale)
+       return "%s", locale.mon[self.month]
+end
+function t:c(locale)
+       return "%.3s %.3s%3d %.2d:%.2d:%.2d %d",
+               locale.abday[self.wday], locale.abmon[self.month],
+               self.day, self.hour, self.min, self.sec, self.year
+end
+-- Century
+function t:C()
+       return "%02d", idiv(self.year, 100)
+end
+function t:d()
+       return "%02d", self.day
+end
+-- Short MM/DD/YY date, equivalent to %m/%d/%y
+function t:D()
+       return "%02d/%02d/%02d", self.month, self.day, self.year % 100
+end
+function t:e()
+       return "%2d", self.day
+end
+-- Short YYYY-MM-DD date, equivalent to %Y-%m-%d
+function t:F()
+       return "%d-%02d-%02d", self.year, self.month, self.day
+end
+-- Week-based year, last two digits (00-99)
+function t:g()
+       return "%02d", iso_8601_week(self) % 100
+end
+-- Week-based year
+function t:G()
+       return "%d", iso_8601_week(self)
+end
+t.h = t.b
+function t:H()
+       return "%02d", self.hour
+end
+function t:I()
+       return "%02d", (self.hour-1) % 12 + 1
+end
+function t:j()
+       return "%03d", self.yday
+end
+function t:m()
+       return "%02d", self.month
+end
+function t:M()
+       return "%02d", self.min
+end
+-- New-line character ('\n')
+function t:n() -- luacheck: ignore 212
+       return "\n"
+end
+function t:p(locale)
+       return self.hour < 12 and locale.am_pm[1] or locale.am_pm[2]
+end
+-- TODO: should respect locale
+function t:r(locale)
+       return "%02d:%02d:%02d %s",
+               (self.hour-1) % 12 + 1, self.min, self.sec,
+               self.hour < 12 and locale.am_pm[1] or locale.am_pm[2]
+end
+-- 24-hour HH:MM time, equivalent to %H:%M
+function t:R()
+       return "%02d:%02d", self.hour, self.min
+end
+function t:s()
+       return "%d", self:timestamp()
+end
+function t:S()
+       return "%02d", self.sec
+end
+-- Horizontal-tab character ('\t')
+function t:t() -- luacheck: ignore 212
+       return "\t"
+end
+-- ISO 8601 time format (HH:MM:SS), equivalent to %H:%M:%S
+function t:T()
+       return "%02d:%02d:%02d", self.hour, self.min, self.sec
+end
+function t:u()
+       return "%d", iso_8601_weekday(self.wday)
+end
+-- Week number with the first Sunday as the first day of week one (00-53)
+function t:U()
+       return "%02d", idiv(self.yday - self.wday + 7, 7)
+end
+-- ISO 8601 week number (00-53)
+function t:V()
+       return "%02d", select(2, iso_8601_week(self))
+end
+-- Weekday as a decimal number with Sunday as 0 (0-6)
+function t:w()
+       return "%d", self.wday - 1
+end
+-- Week number with the first Monday as the first day of week one (00-53)
+function t:W()
+       return "%02d", idiv(self.yday - iso_8601_weekday(self.wday) + 7, 7)
+end
+-- TODO make t.x and t.X respect locale
+t.x = t.D
+t.X = t.T
+function t:y()
+       return "%02d", self.year % 100
+end
+function t:Y()
+       return "%d", self.year
+end
+-- TODO timezones
+function t:z() -- luacheck: ignore 212
+       return "+0000"
+end
+function t:Z() -- luacheck: ignore 212
+       return "GMT"
+end
+-- A literal '%' character.
+t["%"] = function(self) -- luacheck: ignore 212
+       return "%%"
+end
+
+local function strftime(format_string, timetable)
+       return (string.gsub(format_string, "%%([EO]?)(.)", function(locale_modifier, specifier)
+               local func = t[specifier]
+               if func then
+                       return strformat(func(timetable, c_locale))
+               else
+                       error("invalid conversation specifier '%"..locale_modifier..specifier.."'", 3)
+               end
+       end))
+end
+
+local function asctime(timetable)
+       -- Equivalent to the format string "%c\n"
+       return strformat(t.c(timetable, c_locale)) .. "\n"
+end
+
+return {
+       strftime = strftime;
+       asctime = asctime;
+}
diff --git a/.config/awesome/modules/luatz/luatz/timetable.lua b/.config/awesome/modules/luatz/luatz/timetable.lua
new file mode 100644 (file)
index 0000000..1a304e2
--- /dev/null
@@ -0,0 +1,254 @@
+local strftime = require "luatz.strftime".strftime
+local strformat = string.format
+local floor = math.floor
+local idiv do
+       -- Try and use actual integer division when available (Lua 5.3+)
+       local idiv_loader = (loadstring or load)([[return function(n,d) return n//d end]], "idiv") -- luacheck: ignore 113
+       if idiv_loader then
+               idiv = idiv_loader()
+       else
+               idiv = function(n, d)
+                       return floor(n/d)
+               end
+       end
+end
+
+
+local mon_lengths = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
+-- Number of days in year until start of month; not corrected for leap years
+local months_to_days_cumulative = {0}
+for i = 2, 12 do
+       months_to_days_cumulative[i] = months_to_days_cumulative[i-1] + mon_lengths[i-1]
+end
+-- For Sakamoto's Algorithm (day of week)
+local sakamoto = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
+
+local function is_leap(y)
+       if (y % 4) ~= 0 then
+               return false
+       elseif (y % 100) ~= 0 then
+               return true
+       else
+               return (y % 400) == 0
+       end
+end
+
+local function month_length(m, y)
+       if m == 2 then
+               return is_leap(y) and 29 or 28
+       else
+               return mon_lengths[m]
+       end
+end
+
+local function leap_years_since(year)
+       return idiv(year, 4) - idiv(year, 100) + idiv(year, 400)
+end
+
+local function day_of_year(day, month, year)
+       local yday = months_to_days_cumulative[month]
+       if month > 2 and is_leap(year) then
+               yday = yday + 1
+       end
+       return yday + day
+end
+
+local function day_of_week(day, month, year)
+       if month < 3 then
+               year = year - 1
+       end
+       return(year + leap_years_since(year) + sakamoto[month] + day) % 7 + 1
+end
+
+local function borrow(tens, units, base)
+       local frac = tens % 1
+       units = units + frac * base
+       tens = tens - frac
+       return tens, units
+end
+
+local function carry(tens, units, base)
+       if units >= base then
+               tens  = tens + idiv(units, base)
+               units = units % base
+       elseif units < 0 then
+               tens  = tens + idiv(units, base)
+               units = (base + units) % base
+       end
+       return tens, units
+end
+
+-- Modify parameters so they all fit within the "normal" range
+local function normalise(year, month, day, hour, min, sec)
+       -- `month` and `day` start from 1, need -1 and +1 so it works modulo
+       month, day = month - 1, day - 1
+
+       -- Convert everything (except seconds) to an integer
+       -- by propagating fractional components down.
+       year , month = borrow(year , month, 12)
+       -- Carry from month to year first, so we get month length correct in next line around leap years
+       year , month = carry(year, month, 12)
+       month, day   = borrow(month, day  , month_length(floor(month + 1), year))
+       day  , hour  = borrow(day  , hour , 24)
+       hour , min   = borrow(hour , min  , 60)
+       min  , sec   = borrow(min  , sec  , 60)
+
+       -- Propagate out of range values up
+       -- e.g. if `min` is 70, `hour` increments by 1 and `min` becomes 10
+       -- This has to happen for all columns after borrowing, as lower radixes may be pushed out of range
+       min  , sec   = carry(min , sec , 60) -- TODO: consider leap seconds?
+       hour , min   = carry(hour, min , 60)
+       day  , hour  = carry(day , hour, 24)
+       -- Ensure `day` is not underflowed
+       -- Add a whole year of days at a time, this is later resolved by adding months
+       -- TODO[OPTIMIZE]: This could be slow if `day` is far out of range
+       while day < 0 do
+               month = month - 1
+               if month < 0 then
+                       year = year - 1
+                       month = 11
+               end
+               day = day + month_length(month + 1, year)
+       end
+       year, month = carry(year, month, 12)
+
+       -- TODO[OPTIMIZE]: This could potentially be slow if `day` is very large
+       while true do
+               local i = month_length(month + 1, year)
+               if day < i then break end
+               day = day - i
+               month = month + 1
+               if month >= 12 then
+                       month = 0
+                       year = year + 1
+               end
+       end
+
+       -- Now we can place `day` and `month` back in their normal ranges
+       -- e.g. month as 1-12 instead of 0-11
+       month, day = month + 1, day + 1
+
+       return year, month, day, hour, min, sec
+end
+
+local leap_years_since_1970 = leap_years_since(1970)
+local function timestamp(year, month, day, hour, min, sec)
+       year, month, day, hour, min, sec = normalise(year, month, day, hour, min, sec)
+
+       local days_since_epoch = day_of_year(day, month, year)
+               + 365 * (year - 1970)
+               -- Each leap year adds one day
+               + (leap_years_since(year - 1) - leap_years_since_1970) - 1
+
+       return days_since_epoch * (60*60*24)
+               + hour * (60*60)
+               + min  * 60
+               + sec
+end
+
+
+local timetable_methods = {}
+
+function timetable_methods:unpack()
+       return assert(self.year , "year required"),
+               assert(self.month, "month required"),
+               assert(self.day  , "day required"),
+               self.hour or 12,
+               self.min  or 0,
+               self.sec  or 0,
+               self.yday,
+               self.wday
+end
+
+function timetable_methods:normalise()
+       local year, month, day
+       year, month, day, self.hour, self.min, self.sec = normalise(self:unpack())
+
+       self.day   = day
+       self.month = month
+       self.year  = year
+       self.yday  = day_of_year(day, month, year)
+       self.wday  = day_of_week(day, month, year)
+
+       return self
+end
+timetable_methods.normalize = timetable_methods.normalise -- American English
+
+function timetable_methods:timestamp()
+       return timestamp(self:unpack())
+end
+
+function timetable_methods:rfc_3339()
+       local year, month, day, hour, min, fsec = self:unpack()
+       local sec, msec = borrow(fsec, 0, 1000)
+       msec = math.floor(msec)
+       return strformat("%04u-%02u-%02uT%02u:%02u:%02d.%03d", year, month, day, hour, min, sec, msec)
+end
+
+function timetable_methods:strftime(format_string)
+       return strftime(format_string, self)
+end
+
+local timetable_mt
+
+local function coerce_arg(t)
+       if getmetatable(t) == timetable_mt then
+               return t:timestamp()
+       end
+       return t
+end
+
+timetable_mt = {
+       __index    = timetable_methods;
+       __tostring = timetable_methods.rfc_3339;
+       __eq = function(a, b)
+               return a:timestamp() == b:timestamp()
+       end;
+       __lt = function(a, b)
+               return a:timestamp() < b:timestamp()
+       end;
+       __sub = function(a, b)
+               return coerce_arg(a) - coerce_arg(b)
+       end;
+}
+
+local function cast_timetable(tm)
+       return setmetatable(tm, timetable_mt)
+end
+
+local function new_timetable(year, month, day, hour, min, sec, yday, wday)
+       return cast_timetable {
+               year  = year;
+               month = month;
+               day   = day;
+               hour  = hour;
+               min   = min;
+               sec   = sec;
+               yday  = yday;
+               wday  = wday;
+       }
+end
+
+function timetable_methods:clone()
+       return new_timetable(self:unpack())
+end
+
+local function new_from_timestamp(ts)
+       if type(ts) ~= "number" then
+               error("bad argument #1 to 'new_from_timestamp' (number expected, got " .. type(ts) .. ")", 2)
+       end
+       return new_timetable(1970, 1, 1, 0, 0, ts):normalise()
+end
+
+return {
+       is_leap = is_leap;
+       day_of_year = day_of_year;
+       day_of_week = day_of_week;
+       normalise = normalise;
+       timestamp = timestamp;
+
+       new = new_timetable;
+       new_from_timestamp = new_from_timestamp;
+       cast = cast_timetable;
+       timetable_mt = timetable_mt;
+}
diff --git a/.config/awesome/modules/luatz/luatz/tzcache.lua b/.config/awesome/modules/luatz/luatz/tzcache.lua
new file mode 100644 (file)
index 0000000..ae32dce
--- /dev/null
@@ -0,0 +1,35 @@
+local read_tzfile = require "luatz.tzfile".read_tzfile
+
+local base_zoneinfo_path = "/usr/share/zoneinfo/"
+local local_zoneinfo_path = "/etc/localtime"
+local tz_cache = {}
+
+local function name_to_zoneinfo_path(name)
+       if name == nil then
+               return local_zoneinfo_path
+       elseif name:sub(1, 1) == "/" then
+               return name
+       else
+               return base_zoneinfo_path .. name
+       end
+end
+
+local function clear_tz_cache(name)
+       tz_cache[name_to_zoneinfo_path(name)] = nil
+end
+
+local function get_tz(name)
+       local path = name_to_zoneinfo_path(name)
+       -- TODO: stat path
+       local tzinfo = tz_cache[path]
+       if tzinfo == nil then
+               tzinfo = read_tzfile(path)
+               tz_cache[path] = tzinfo
+       end
+       return tzinfo
+end
+
+return {
+       get_tz = get_tz;
+       clear_tz_cache = clear_tz_cache;
+}
diff --git a/.config/awesome/modules/luatz/luatz/tzfile.lua b/.config/awesome/modules/luatz/luatz/tzfile.lua
new file mode 100644 (file)
index 0000000..e57db6d
--- /dev/null
@@ -0,0 +1,251 @@
+local tz_info_mt = require "luatz.tzinfo".tz_info_mt
+local tt_info_mt = require "luatz.tzinfo".tt_info_mt
+
+local read_int32be, read_int64be
+
+-- luacheck: push std max
+if string.unpack then
+       -- Only available in Lua 5.3+
+       function read_int32be(fd)
+               local data, err = fd:read(4)
+               if data == nil then return nil, err end
+               return string.unpack(">i4", data)
+       end
+
+       function read_int64be(fd)
+               local data, err = fd:read(8)
+               if data == nil then return nil, err end
+               return string.unpack(">i8", data)
+       end
+else -- luacheck: pop
+       function read_int32be(fd)
+               local data, err = fd:read(4)
+               if data == nil then return nil, err end
+               local o1, o2, o3, o4 = data:byte(1, 4)
+
+               local unsigned = o4 + o3*2^8 + o2*2^16 + o1*2^24
+               if unsigned >= 2^31 then
+                       return unsigned - 2^32
+               else
+                       return unsigned
+               end
+       end
+
+       function read_int64be(fd)
+               local data, err = fd:read(8)
+               if data == nil then return nil, err end
+               local o1, o2, o3, o4, o5, o6, o7, o8 = data:byte(1, 8)
+
+               local unsigned = o8 + o7*2^8 + o6*2^16 + o5*2^24 + o4*2^32 + o3*2^40 + o2*2^48 + o1*2^56
+               if unsigned >= 2^63 then
+                       return unsigned - 2^64
+               else
+                       return unsigned
+               end
+       end
+end
+
+local function read_flags(fd, n)
+       local data, err = fd:read(n)
+       if data == nil then return nil, err end
+
+       local res = {}
+       for i=1, n do
+               res[i] = data:byte(i,i) ~= 0
+       end
+       return res
+end
+
+local fifteen_nulls = ("\0"):rep(15)
+local function read_tz(fd)
+       assert(fd:read(4) == "TZif", "Invalid TZ file")
+       local version = assert(fd:read(1))
+       if version == "\0" or version == "2" or version == "3" then
+               local MIN_TIME = -2^32+1
+
+               assert(assert(fd:read(15)) == fifteen_nulls, "Expected 15 nulls")
+
+               -- The number of UTC/local indicators stored in the file.
+               local tzh_ttisgmtcnt = assert(read_int32be(fd))
+
+               -- The number of standard/wall indicators stored in the file.
+               local tzh_ttisstdcnt = assert(read_int32be(fd))
+
+               -- The number of leap seconds for which data is stored in the file.
+               local tzh_leapcnt = assert(read_int32be(fd))
+
+               -- The number of "transition times" for which data is stored in the file.
+               local tzh_timecnt = assert(read_int32be(fd))
+
+               -- The number of "local time types" for which data is stored in the file (must not be zero).
+               local tzh_typecnt = assert(read_int32be(fd))
+
+               -- The number of characters of "timezone abbreviation strings" stored in the file.
+               local tzh_charcnt = assert(read_int32be(fd))
+
+               local transition_times = {}
+               for i=1, tzh_timecnt do
+                       transition_times[i] = assert(read_int32be(fd))
+               end
+               local transition_time_ind = {assert(fd:read(tzh_timecnt)):byte(1, -1)}
+
+               local ttinfos = {}
+               for i=1, tzh_typecnt do
+                       ttinfos[i] = {
+                               gmtoff = assert(read_int32be(fd));
+                               isdst  = assert(fd:read(1)) ~= "\0";
+                               abbrind = assert(fd:read(1)):byte();
+                       }
+               end
+
+               local abbreviations = assert(fd:read(tzh_charcnt))
+
+               local leap_seconds = {} -- luacheck: ignore 241
+               for i=1, tzh_leapcnt do
+                       leap_seconds[i] = {
+                               offset = assert(read_int32be(fd));
+                               n = assert(read_int32be(fd));
+                       }
+               end
+
+               local isstd = assert(read_flags(fd, tzh_ttisstdcnt))
+
+               local isgmt = assert(read_flags(fd, tzh_ttisgmtcnt))
+
+               local TZ
+
+               if version == "2" or version == "3" then
+                       --[[
+                       For version-2-format timezone files, the above header and data is followed by a second header and data,
+                       identical in format except that eight bytes are used for each transition time or leap-second time.
+                       ]]
+                       assert(fd:read(4) == "TZif")
+                       assert(fd:read(1) == version)
+                       assert(assert(fd:read(15)) == fifteen_nulls, "Expected 15 nulls")
+
+                       MIN_TIME = -2^64+1
+
+                       -- The number of UTC/local indicators stored in the file.
+                       tzh_ttisgmtcnt = assert(read_int32be(fd))
+
+                       -- The number of standard/wall indicators stored in the file.
+                       tzh_ttisstdcnt = assert(read_int32be(fd))
+
+                       -- The number of leap seconds for which data is stored in the file.
+                       tzh_leapcnt = assert(read_int32be(fd))
+
+                       -- The number of "transition times" for which data is stored in the file.
+                       tzh_timecnt = assert(read_int32be(fd))
+
+                       -- The number of "local time types" for which data is stored in the file (must not be zero).
+                       tzh_typecnt = assert(read_int32be(fd))
+
+                       -- The number of characters of "timezone abbreviation strings" stored in the file.
+                       tzh_charcnt = assert(read_int32be(fd))
+
+                       transition_times = {}
+                       for i=1, tzh_timecnt do
+                               transition_times[i] = assert(read_int64be(fd))
+                       end
+                       transition_time_ind = {assert(fd:read(tzh_timecnt)):byte(1, -1)}
+
+                       ttinfos = {}
+                       for i=1, tzh_typecnt do
+                               ttinfos[i] = {
+                                       gmtoff = assert(read_int32be(fd));
+                                       isdst  = assert(fd:read(1)) ~= "\0";
+                                       abbrind = assert(fd:read(1)):byte();
+                               }
+                       end
+
+                       abbreviations = assert(fd:read(tzh_charcnt))
+
+                       leap_seconds = {}
+                       for i=1, tzh_leapcnt do
+                               leap_seconds[i] = {
+                                       offset = assert(read_int64be(fd));
+                                       n = assert(read_int32be(fd));
+                               }
+                       end
+
+                       isstd = assert(read_flags(fd, tzh_ttisstdcnt))
+
+                       isgmt = assert(read_flags(fd, tzh_ttisgmtcnt))
+
+                       --[[
+                       After the second header and data comes a newline-enclosed, POSIX-TZ-environment-variable-style string
+                       for use in handling instants after the last transition time stored in the file
+                       (with nothing between the newlines if there is no POSIX representation for such instants).
+                       ]]
+
+                       --[[
+                       For version-3-format time zone files, the POSIX-TZ-style string may
+                       use two minor extensions to the POSIX TZ format, as described in newtzset (3).
+                       First, the hours part of its transition times may be signed and range from
+                       -167 through 167 instead of the POSIX-required unsigned values
+                       from 0 through 24.  Second, DST is in effect all year if it starts
+                       January 1 at 00:00 and ends December 31 at 24:00 plus the difference
+                       between daylight saving and standard time.
+                       ]]
+
+                       assert(assert(fd:read(1)) == "\n", "Expected newline at end of version 2 header")
+
+                       TZ = assert(fd:read("*l"))
+                       if #TZ == 0 then
+                               TZ = nil
+                       end
+               end
+
+               for i=1, tzh_typecnt do
+                       local v = ttinfos[i]
+                       v.abbr = abbreviations:sub(v.abbrind+1, v.abbrind+3)
+                       v.isstd = isstd[i] or false
+                       v.isgmt = isgmt[i] or false
+                       setmetatable(v, tt_info_mt)
+               end
+
+               --[[
+               Use the first standard-time ttinfo structure in the file
+               (or simply the first ttinfo structure in the absence of a standard-time structure)
+               if either tzh_timecnt is zero or the time argument is less than the first transition time recorded in the file.
+               ]]
+               local first = 1
+               do
+                       for i=1, tzh_ttisstdcnt do
+                               if isstd[i] then
+                                       first = i
+                                       break
+                               end
+                       end
+               end
+
+               local res = {
+                       future = TZ;
+                       [0] = {
+                               transition_time = MIN_TIME;
+                               info = ttinfos[first];
+                       }
+               }
+               for i=1, tzh_timecnt do
+                       res[i] = {
+                               transition_time = transition_times[i];
+                               info = ttinfos[transition_time_ind[i]+1];
+                       }
+               end
+               return setmetatable(res, tz_info_mt)
+       else
+               error("Unsupported version")
+       end
+end
+
+local function read_tzfile(path)
+       local fd = assert(io.open(path, "rb"))
+       local tzinfo = read_tz(fd)
+       fd:close()
+       return tzinfo
+end
+
+return {
+       read_tz = read_tz;
+       read_tzfile = read_tzfile;
+}
diff --git a/.config/awesome/modules/luatz/luatz/tzinfo.lua b/.config/awesome/modules/luatz/luatz/tzinfo.lua
new file mode 100644 (file)
index 0000000..e37483f
--- /dev/null
@@ -0,0 +1,97 @@
+local gettime = require "luatz.gettime".gettime
+local timetable_mt = require "luatz.timetable".timetable_mt
+
+local function to_timestamp(o)
+       if type(o) == "number" then
+               return o
+       elseif getmetatable(o) == timetable_mt then
+               return o:timestamp()
+       end
+end
+
+local tz_info_methods = { }
+local tz_info_mt = {
+       __name = "luatz.tz_info";
+       __index = tz_info_methods;
+}
+local tt_info_mt = {
+       __name = "luatz.tt_info";
+       __tostring = function(self)
+               return string.format("tt_info:%s=%d", self.abbr, self.gmtoff)
+       end;
+}
+
+-- Binary search
+local function find_current(tzinfo, target, i, j)
+       if i >= j then return j end
+
+       local half = math.ceil((j+i) / 2)
+
+       if target >= tzinfo[half].transition_time then
+               return find_current(tzinfo, target, half, j)
+       else
+               return find_current(tzinfo, target, i, half-1)
+       end
+end
+
+local function find_current_local(tzinfo, ts_local)
+       -- Find two best possibilities by searching back and forward a day (assumes transition is never by more than 24 hours)
+       local tz_first = find_current(tzinfo, ts_local-86400, 0, #tzinfo)
+       local tz_last  = find_current(tzinfo, ts_local+86400, 0, #tzinfo)
+
+       local n_candidates = tz_last - tz_first + 1
+
+       if n_candidates == 1 then
+               return tz_first
+       elseif n_candidates == 2 then
+               local tz_first_ob = tzinfo[tz_first]
+               local tz_last_ob  = tzinfo[tz_last]
+
+               local first_gmtoffset = tz_first_ob.info.gmtoff
+               local last_gmtoffset  = tz_last_ob .info.gmtoff
+
+               local t_start = tz_last_ob.transition_time + first_gmtoffset
+               local t_end   = tz_last_ob.transition_time + last_gmtoffset
+
+               -- If timestamp is before start or after end
+               if ts_local < t_start then
+                       return tz_first
+               elseif ts_local > t_end then
+                       return tz_last
+               end
+
+               -- If we get this far, the local time is ambiguous
+               return tz_first, tz_last
+       else
+               error("Too many transitions in a 2 day period")
+       end
+end
+
+function tz_info_methods:find_current(current)
+       current = assert(to_timestamp(current), "invalid timestamp to :find_current")
+       return self[find_current(self, current, 0, #self)].info
+end
+
+function tz_info_methods:localise(utc_ts)
+       utc_ts = utc_ts or gettime()
+       return utc_ts + self:find_current(utc_ts).gmtoff
+end
+tz_info_methods.localize = tz_info_methods.localise
+
+function tz_info_methods:utctime(ts_local)
+       ts_local = assert(to_timestamp(ts_local), "invalid timestamp to :utctime")
+       local tz1, tz2 = find_current_local(self, ts_local)
+       tz1 = self[tz1].info
+       if tz2 == nil then
+               return ts_local - tz1.gmtoff
+       else -- Local time is ambiguous
+               tz2 = self[tz2].info
+
+               return ts_local - tz2.gmtoff, ts_local - tz2.gmtoff
+       end
+end
+
+return {
+       tz_info_mt = tz_info_mt;
+       tt_info_mt = tt_info_mt;
+}
diff --git a/.config/awesome/modules/luatz/spec/Godthab.tz b/.config/awesome/modules/luatz/spec/Godthab.tz
new file mode 100644 (file)
index 0000000..111d9a8
Binary files /dev/null and b/.config/awesome/modules/luatz/spec/Godthab.tz differ
diff --git a/.config/awesome/modules/luatz/spec/parse_spec.lua b/.config/awesome/modules/luatz/spec/parse_spec.lua
new file mode 100644 (file)
index 0000000..e38ad38
--- /dev/null
@@ -0,0 +1,17 @@
+describe("Time parsing library", function()
+       local timetable = require "luatz.timetable"
+       local parse = require "luatz.parse"
+
+       it("#RFC3339 parsing", function()
+               assert.same(timetable.new(2013,10,22,14,17,02), (parse.rfc_3339 "2013-10-22T14:17:02Z"))
+
+               -- Numeric offsets accepted
+               assert.same({timetable.new(2013,10,22,14,17,02), 10*3600 }, {parse.rfc_3339 "2013-10-22T14:17:02+10:00" })
+
+               -- Missing offsets parse
+               assert.same(timetable.new(2013,10,22,14,17,02), (parse.rfc_3339 "2013-10-22T14:17:02"))
+
+               -- Invalid
+               assert.same(nil, (parse.rfc_3339 "an invalid timestamp"))
+       end)
+end)
diff --git a/.config/awesome/modules/luatz/spec/strftime_spec.lua b/.config/awesome/modules/luatz/spec/strftime_spec.lua
new file mode 100644 (file)
index 0000000..39f92c7
--- /dev/null
@@ -0,0 +1,30 @@
+local luatz = require "luatz.init"
+local time = 1234567890
+local base_tt = luatz.gmtime(time)
+describe("#strftime works the same as os.date", function()
+       local strftime = luatz.strftime.strftime
+       for _, spec in ipairs {
+               "a", "A", "b", "B", "c", "C", "d", "D", "e", "F",
+               "g", "G", "H", "I", "j", "m", "M", "n", "p", "r",
+               "R", --[["s",]] "S", "t", "T", "u", "U", "V", "w", "W",
+               "y", "Y", "z", "Z" , "%"
+       } do
+               local tt = base_tt:clone()
+               local f = "%"..spec
+               local osdf = "!%"..spec
+               it("format specifier '"..f.."' is equivalent to os.date('"..osdf.."')", function()
+                       for i=1, 365*12 do
+                               local t = time + 60*60*24*i
+                               tt.day = tt.day + 1
+                               tt:normalise()
+                               assert.are.same(os.date(osdf,t), strftime(f,tt))
+                       end
+               end)
+       end
+end)
+describe("#asctime", function()
+       local asctime = luatz.strftime.asctime
+       it("should format correctly", function()
+               assert.are.same("Fri Feb 13 23:31:30 2009\n", asctime(base_tt))
+       end)
+end)
diff --git a/.config/awesome/modules/luatz/spec/timetable_spec.lua b/.config/awesome/modules/luatz/spec/timetable_spec.lua
new file mode 100644 (file)
index 0000000..b7bc3e3
--- /dev/null
@@ -0,0 +1,138 @@
+describe("Timetable library", function()
+       local timetable = require "luatz.timetable"
+
+       local function native_normalise(year, month, day)
+               return os.date("*t",os.time {
+                       year = year;
+                       month = month;
+                       day = day;
+               })
+       end
+
+       it("#is_leap is correct", function()
+               assert.same(false, timetable.is_leap(1))
+               assert.same(false, timetable.is_leap(3))
+               assert.same(true , timetable.is_leap(4))
+               assert.same(true , timetable.is_leap(2000))
+               assert.same(true , timetable.is_leap(2004))
+               assert.same(true , timetable.is_leap(2012))
+               assert.same(false, timetable.is_leap(2013))
+               assert.same(false, timetable.is_leap(2014))
+               assert.same(false, timetable.is_leap(2100))
+               assert.same(true , timetable.is_leap(2400))
+       end)
+
+       it("#normalise gets #wday (day of week) correct", function()
+
+               local function assert_same_wday(year, month, day)
+                       return assert.are.same(
+                               native_normalise(year, month, day).wday,
+                               timetable.new(year, month, day):normalise().wday
+                       )
+               end
+
+               assert_same_wday(2013, 7, 23)
+               assert_same_wday(2013, 7, 24)
+               assert_same_wday(2013, 7, 25)
+               assert_same_wday(2013, 7, 26)
+               assert_same_wday(2013, 7, 27)
+               assert_same_wday(2013, 7, 28)
+               assert_same_wday(2013, 7, 29)
+               assert_same_wday(2014, 1, 1)
+               assert_same_wday(2014, 1, 6)
+               assert_same_wday(2016, 2, 28)
+               assert_same_wday(2016, 2, 29)
+               assert_same_wday(2016, 3, 1)
+       end)
+
+       local function native_timestamp(year, month, day)
+               return assert(tonumber(assert(io.popen(
+                                               string.format('date -u -d "%d-%d-%d" +%%s', year, month, day)
+                                       )):read "*l"))
+       end
+
+       it("#timestamp creation is valid", function()
+               for y=1950,2013 do
+                       for m=1,12 do
+                               assert.same(native_timestamp(y,m,1), timetable.timestamp(y,m,1,0,0,0))
+                       end
+               end
+       end)
+
+       it("#normalise handles out of range days in a year", function()
+               assert.same({2014,1,1,0,0,0}, {timetable.normalise(2013,1,366,0,0,0)})
+               assert.same({2014,2,4,0,0,0}, {timetable.normalise(2013,1,400,0,0,0)})
+               assert.same({2017,2,3,0,0,0}, {timetable.normalise(2016,1,400,0,0,0)})
+               assert.same({2016,3,5,0,0,0}, {timetable.normalise(2015,1,430,0,0,0)})
+               assert.same({2017,3,5,0,0,0}, {timetable.normalise(2016,1,430,0,0,0)})
+               assert.same({2027,5,18,0,0,0}, {timetable.normalise(2000,1,10000,0,0,0)})
+               assert.same({29379,1,25,0,0,0}, {timetable.normalise(2000,1,10000000,0,0,0)})
+       end)
+
+       it("#normalise handles out of range days in a #month", function()
+               assert.same({2012,12,1,0,0,0}, {timetable.normalise(2013,0,1,0,0,0)})
+               assert.same({2016,6,1,0,0,0}, {timetable.normalise(2013,42,1,0,0,0)})
+
+               -- Correct behaviour around leap days
+               assert.same({2012,3,23,0,0,0}, {timetable.normalise(2012,2,52,0,0,0)})
+               assert.same({2013,3,24,0,0,0}, {timetable.normalise(2013,2,52,0,0,0)})
+
+               assert.same({2012,2,27,0,0,0}, {timetable.normalise(2012,3,-2,0,0,0)})
+               assert.same({2013,2,26,0,0,0}, {timetable.normalise(2013,3,-2,0,0,0)})
+
+               -- Also when more fields are out of range
+               assert.same({2016,7,22,0,0,0}, {timetable.normalise(2013,42,52,0,0,0)})
+               assert.same({2016,7,24,2,0,0}, {timetable.normalise(2013,42,52,50,0,0)})
+       end)
+
+       it("#normalise handles fractional #month", function()
+               assert.same({2015,2,15,0,0,0}, {timetable.normalise(2014,14.5,1,0,0,0)})
+               assert.same({2016,2,15,12,0,0}, {timetable.normalise(2015,14.5,1,0,0,0)}) -- leap year, so hours is 12
+               assert.same({2017,2,15,0,0,0}, {timetable.normalise(2016,14.5,1,0,0,0)})
+       end)
+
+       it("#normalise handles negative carry (issue #10)", function()
+               assert.same({1970,01,01,00,59,00}, {timetable.normalise(1970,01,01,01,00,-60)})
+               assert.same({1970,01,01,00,58,58}, {timetable.normalise(1970,01,01,01,00,-62)})
+               assert.same({1969,12,31,23,55,58}, {timetable.normalise(1970,01,01,01,-63,-62)})
+               assert.same({2017,02,3,0,0,0}, {timetable.normalise(2017,02,13,0,-14400,0)})
+       end)
+
+       it("#normalise handles negative day carry (issue #13)", function()
+               assert.same({2016,11,30,00,00,00}, {timetable.normalise(2016,12,0,0,0,0)})
+               assert.same({2017,11,30,00,00,00}, {timetable.normalise(2017,12,0,0,0,0)})
+               assert.same({2018,11,30,00,00,00}, {timetable.normalise(2018,12,0,0,0,0)})
+
+               assert.same({2017,2,13,0,0,0}, {timetable.normalise(2017,3,-15,0,0,0)})
+               assert.same({2016,10,1,0,0,0}, {timetable.normalise(2017,3,-150,0,0,0)})
+               assert.same({2013,1,20,0,0,0}, {timetable.normalise(2017,3,-1500,0,0,0)})
+               assert.same({1976,2,4,0,0,0}, {timetable.normalise(2017,3,-15000,0,0,0)})
+               assert.same({1606,6,23,0,0,0}, {timetable.normalise(2017,3,-150000,0,0,0)})
+       end)
+
+       local function round_trip_add(t, field, x)
+               local before = t:clone()
+               t[field]=t[field]+x;
+               t:normalise();
+               t[field]=t[field]-x;
+               t:normalise();
+               assert.same(0, t-before)
+       end
+       it("#normalise round trips", function()
+               round_trip_add(timetable.new(2000,2,28,0,0,0), "month", 0.5)
+               round_trip_add(timetable.new(2014,8,28,19,23,0), "month", 0.4)
+               round_trip_add(timetable.new(2014,14.5,28,0,0,0), "month", 0.4)
+       end)
+
+       it("#rfc_3339 works with fractional milliseconds", function()
+               -- on lua 5.3 this used to throw an error due to milliseconds not being an integer
+               timetable.new_from_timestamp(1415141759.999911111):rfc_3339()
+       end)
+
+       it("#rfc_3339 doesn't round seconds up to 60 (issue #4)", function()
+               assert.same("2014-11-04T22:55:59.999", timetable.new_from_timestamp(1415141759.999911111):rfc_3339())
+               assert.same("1970-01-01T00:00:59.999", timetable.new_from_timestamp(59.9999999):rfc_3339())
+               assert.same("1969-12-31T23:59:59.999", timetable.new_from_timestamp(-0.001):rfc_3339())
+               assert.same("1969-12-31T23:59:00.000", timetable.new_from_timestamp(-59.9999999):rfc_3339())
+       end)
+end)
diff --git a/.config/awesome/modules/luatz/spec/tzcache_spec.lua b/.config/awesome/modules/luatz/spec/tzcache_spec.lua
new file mode 100644 (file)
index 0000000..ea0c211
--- /dev/null
@@ -0,0 +1,17 @@
+describe("Opening/reading system files", function()
+       local tzcache = require "luatz.tzcache"
+       it("should have a localtime", function()
+               tzcache.get_tz()
+       end)
+       it("should be able to open UTC", function()
+               tzcache.get_tz("UTC")
+       end)
+       it("should re-use results from cache", function()
+               -- If cached it should return the same table
+               local localtime = tzcache.get_tz()
+               assert.are.equal(localtime, tzcache.get_tz())
+               -- Once cache is cleared it should return a new table
+               tzcache.clear_tz_cache()
+               assert._not.equal(localtime, tzcache.get_tz())
+       end)
+end)
diff --git a/.config/awesome/modules/luatz/spec/tzfile_spec.lua b/.config/awesome/modules/luatz/spec/tzfile_spec.lua
new file mode 100644 (file)
index 0000000..4f10b71
--- /dev/null
@@ -0,0 +1,8 @@
+describe("Opening/reading tz files", function()
+       local tzfile = require "luatz.tzfile"
+       it("should be able to open a version 3 file", function()
+               -- The tz file for America/Godthab from 2015g
+               -- One of the smallest tzif3 files I have
+               tzfile.read_tzfile("spec/Godthab.tz")
+       end)
+end)