]> 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:
1  2 
.config/awesome/modules/luatz/.busted
.config/awesome/modules/luatz/.gitignore
.config/awesome/modules/luatz/.luacheckrc
.config/awesome/modules/luatz/.luacov
.config/awesome/modules/luatz/.travis.yml
.config/awesome/modules/luatz/COPYING
.config/awesome/modules/luatz/NEWS
.config/awesome/modules/luatz/README.md
.config/awesome/modules/luatz/doc/Makefile
.config/awesome/modules/luatz/doc/README.md
.config/awesome/modules/luatz/doc/gettime.md
.config/awesome/modules/luatz/doc/index.md
.config/awesome/modules/luatz/doc/links.md
.config/awesome/modules/luatz/doc/metadata.yaml
.config/awesome/modules/luatz/doc/parse.md
.config/awesome/modules/luatz/doc/site.css
.config/awesome/modules/luatz/doc/template.html
.config/awesome/modules/luatz/doc/timetable.md
.config/awesome/modules/luatz/doc/tzinfo.md
.config/awesome/modules/luatz/examples/date_arithmetic.lua
.config/awesome/modules/luatz/examples/os_date.lua
.config/awesome/modules/luatz/luatz-scm-0.rockspec
.config/awesome/modules/luatz/luatz/gettime.lua
.config/awesome/modules/luatz/luatz/init.lua
.config/awesome/modules/luatz/luatz/parse.lua
.config/awesome/modules/luatz/luatz/strftime.lua
.config/awesome/modules/luatz/luatz/timetable.lua
.config/awesome/modules/luatz/luatz/tzcache.lua
.config/awesome/modules/luatz/luatz/tzfile.lua
.config/awesome/modules/luatz/luatz/tzinfo.lua
.config/awesome/modules/luatz/spec/Godthab.tz
.config/awesome/modules/luatz/spec/parse_spec.lua
.config/awesome/modules/luatz/spec/strftime_spec.lua
.config/awesome/modules/luatz/spec/timetable_spec.lua
.config/awesome/modules/luatz/spec/tzcache_spec.lua
.config/awesome/modules/luatz/spec/tzfile_spec.lua

index 0000000000000000000000000000000000000000,d1a4ad9a4fca254b3b4f73cc3fd2d8d9656145f3..d1a4ad9a4fca254b3b4f73cc3fd2d8d9656145f3
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,5 +1,5 @@@
+ return {
+     default = {
+         lpath = "./?.lua";
+     };
+ }
index 0000000000000000000000000000000000000000,a299fa5e496028a71c0d0247996936b7493732ff..a299fa5e496028a71c0d0247996936b7493732ff
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,4 +1,4 @@@
+ luatz-*.rock
+ doc/luatz.3
+ doc/luatz.html
+ doc/luatz.pdf
index 0000000000000000000000000000000000000000,b1f4e2ee8b2909001ea929800ec9a4414b30e623..b1f4e2ee8b2909001ea929800ec9a4414b30e623
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,2 +1,2 @@@
+ std = "min"
+ files["spec"] = {std = "+busted"}
index 0000000000000000000000000000000000000000,9e2d5413627db74166e6f2781ffabdb603aab0fd..9e2d5413627db74166e6f2781ffabdb603aab0fd
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,10 +1,10 @@@
+ return {
+       statsfile = "luacov.stats.out";
+       reportfile = "luacov.report.out";
+       deletestats = true;
+       include = {
+               "^./luatz/";
+       };
+       exclude = {
+       };
+ }
index 0000000000000000000000000000000000000000,7015a2995f8842a78889aeb087a19252f7a6c919..7015a2995f8842a78889aeb087a19252f7a6c919
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,43 +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
index 0000000000000000000000000000000000000000,b4435bbfcb8de3f9024b00572c608ef91a51d737..b4435bbfcb8de3f9024b00572c608ef91a51d737
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,20 +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.
index 0000000000000000000000000000000000000000,851fc160576d66402d338641320c0246edd2704a..851fc160576d66402d338641320c0246edd2704a
mode 000000,100644..100644
--- /dev/null
--- 2/NEWS
@@@ -1,0 -1,31 +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
index 0000000000000000000000000000000000000000,0c4575c71cb24cb9c0d265294ccca61c1455c139..0c4575c71cb24cb9c0d265294ccca61c1455c139
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,29 +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
index 0000000000000000000000000000000000000000,7d3001572a3f1a5f2f76f16e7d2673f2ee0b10a7..7d3001572a3f1a5f2f76f16e7d2673f2ee0b10a7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,26 +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
index 0000000000000000000000000000000000000000,ed82567d7c5b2454afbe3f300135b0dcb81978c1..ed82567d7c5b2454afbe3f300135b0dcb81978c1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,5 +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.
index 0000000000000000000000000000000000000000,355057773d48d2b91ed282e3f635faca37cb2dfb..355057773d48d2b91ed282e3f635faca37cb2dfb
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,27 +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
index 0000000000000000000000000000000000000000,b69b2d8a76689cfbe4ba083080d61b7455dd80a6..b69b2d8a76689cfbe4ba083080d61b7455dd80a6
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,51 +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
index 0000000000000000000000000000000000000000,3e8905f474f678e6a210e2df71e53508c27ccbf5..3e8905f474f678e6a210e2df71e53508c27ccbf5
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,5 +1,5 @@@
+ # Links
+   - [Github](https://github.com/daurnimator/luatz)
+   - [Issue tracker](https://github.com/daurnimator/luatz/issues)
+   - [luarocks](https://luarocks.org/modules/daurnimator/luatz)
index 0000000000000000000000000000000000000000,4f144f082175ed3d571c4d56cb5638f2b27975e2..4f144f082175ed3d571c4d56cb5638f2b27975e2
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,6 +1,6 @@@
+ ---
+ title: luatz
+ subtitle: A lua library for time and date manipulation
+ author: Daurnimator <quae@daurnimator.com>
+ section: 3
+ ...
index 0000000000000000000000000000000000000000,c41eae56ae11ef629b7e841e5364924f63bd7b37..c41eae56ae11ef629b7e841e5364924f63bd7b37
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,12 +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
index 0000000000000000000000000000000000000000,88580025f99894c59310fb953e304639bf3b3859..88580025f99894c59310fb953e304639bf3b3859
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,156 +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)
+     }
+ }
index 0000000000000000000000000000000000000000,a74a7b6aa9e1cba0d57aea4d7a7b80891fe02a66..a74a7b6aa9e1cba0d57aea4d7a7b80891fe02a66
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,71 +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>
index 0000000000000000000000000000000000000000,812da43c711e4ab9b272cdc770e085efeefbbec6..812da43c711e4ab9b272cdc770e085efeefbbec6
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,71 +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`
index 0000000000000000000000000000000000000000,d4791e2757f54e2a156d67f945b3aa1367412869..d4791e2757f54e2a156d67f945b3aa1367412869
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,45 +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.
index 0000000000000000000000000000000000000000,4065122a18fdbf9e42f169cbf68957dff419f71e..4065122a18fdbf9e42f169cbf68957dff419f71e
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,33 +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")
index 0000000000000000000000000000000000000000,ad254cea78b6ef314835e3333505e8ecd494d339..ad254cea78b6ef314835e3333505e8ecd494d339
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,25 +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
index 0000000000000000000000000000000000000000,2ceae4cbe9e3bab17c54d3a3733c1b2b4344287d..2ceae4cbe9e3bab17c54d3a3733c1b2b4344287d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,40 +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";
+       };
+ }
index 0000000000000000000000000000000000000000,4d6f45ad2c95a9a7494f0cb8c2f01fe30706701c..4d6f45ad2c95a9a7494f0cb8c2f01fe30706701c
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,53 +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
index 0000000000000000000000000000000000000000,4423189304874c46efb00e07f3f52077561549f5..4423189304874c46efb00e07f3f52077561549f5
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,40 +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
index 0000000000000000000000000000000000000000,6feeb293793ab4a6f74ff17e526bae67cd72d052..6feeb293793ab4a6f74ff17e526bae67cd72d052
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,40 +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;
+ }
index 0000000000000000000000000000000000000000,223da0f15e4f82ab04ecb46ca30fa68acb7dc90b..223da0f15e4f82ab04ecb46ca30fa68acb7dc90b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,210 +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;
+ }
index 0000000000000000000000000000000000000000,1a304e21ef58f468b477b32c1667f2cee42cb371..1a304e21ef58f468b477b32c1667f2cee42cb371
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,254 +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;
+ }
index 0000000000000000000000000000000000000000,ae32dceddd08abf8eb65ac52b8eaaddea1260f6c..ae32dceddd08abf8eb65ac52b8eaaddea1260f6c
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,35 +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;
+ }
index 0000000000000000000000000000000000000000,e57db6d8fa82dbcf719eaa2b135825a9e1930499..e57db6d8fa82dbcf719eaa2b135825a9e1930499
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,251 +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;
+ }
index 0000000000000000000000000000000000000000,e37483f81a562828e28b392735b7c9f9c18339b4..e37483f81a562828e28b392735b7c9f9c18339b4
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,97 +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;
+ }
index 0000000000000000000000000000000000000000,111d9a8178ca214aae66804548a990bb2691644b..111d9a8178ca214aae66804548a990bb2691644b
mode 000000,100644..100644
Binary files differ
index 0000000000000000000000000000000000000000,e38ad3842673b3ad5bd5d3d4e75588e927529363..e38ad3842673b3ad5bd5d3d4e75588e927529363
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,17 +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)
index 0000000000000000000000000000000000000000,39f92c7eb3f5824a3db73e88476869de7c4e0012..39f92c7eb3f5824a3db73e88476869de7c4e0012
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,30 +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)
index 0000000000000000000000000000000000000000,b7bc3e382028e7c9957b353ee46847903ae05ef0..b7bc3e382028e7c9957b353ee46847903ae05ef0
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,138 +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)
index 0000000000000000000000000000000000000000,ea0c211a60345e8e359fdfea2d5c896950bcfea5..ea0c211a60345e8e359fdfea2d5c896950bcfea5
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,17 +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)
index 0000000000000000000000000000000000000000,4f10b71cebcd12db67378dab668f8798148caded..4f10b71cebcd12db67378dab668f8798148caded
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,8 +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)