From 0c244a4e12eb65cf72972bdf49b93366a174e9b8 Mon Sep 17 00:00:00 2001 From: aalatchm Date: Tue, 22 Mar 2011 18:27:08 +0000 Subject: added posixtz.lua for parse functionality The parse() function takes a POSIX TZ string and returns a table with TZ parameters. --- posixtz.lua | 192 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 posixtz.lua diff --git a/posixtz.lua b/posixtz.lua new file mode 100644 index 0000000..7eb92dd --- /dev/null +++ b/posixtz.lua @@ -0,0 +1,192 @@ +------------------------------------------------------------------------------- +-- POSIX TZ Parser for Lua +-- Copyright (C) 2011 H. Andrew Latchman +-- +-- Performs some validation of TZ string +-- based on fields to determine offset and dst settings +-- +-- See http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html +-- TZ variable, with no leading colon +-- Syntax: stdoffset[dst[offset][,start[/time],end[/time]]] +-- Examples: 'GMT0' 'EST5EDT,M3.2.0,M11.1.0' '5' +------------------------------------------------------------------------------- + +-- Import Section +-- declare everything this module needs from outside +local error = error +local string = string +local tonumber = tonumber +local pairs = pairs +local ipairs = ipairs + +-- Rest of the code is part of module "posixtz" +module('posixtz') + +function parse ( str ) + + -- create tz table for data to be returned + local tz = {} + -- create temp table for storing what is left to be parsed + local temp = {} + + if not str then + error('Nothing to parse') + end + + + -- Check that values.device.timezone has required fields + -- (technically, std must be at least 3 chars, but we don't care) + + -- Parse out 'std' and validate. + -- check for quoted format of std + tz.std, temp.afterstd = string.match(str, '^(<[%w+-]+>)(.*)') + if not tz.std then + -- try unquoted format of std + tz.std, temp.afterstd = string.match(str, '^(%a+)(.*)') + end + + -- tz.std must be defined by now + if not tz.std then + error('Not a valid POSIX TZ. "std" invalid') + end + + + -- After 'std' there must be [+|-]hh[:mm[:ss]] as the offset + -- the following code simplifies to [+|-]hh[[:][mm[[:][ss]] + tz.offset = {} + tz.offset.sign, tz.offset.hour, tz.offset.min, tz.offset.sec, temp.afteroffset = string.match(temp.afterstd, '^([+-]?)(%d+):?(%d?%d?):?(%d?%d?)(.*)') + + -- tz.offset.hour must be non-empty, the other vars may be '' if not present + if tz.offset.hour and ( tz.offset.hour ~= '' ) then + + tz.offset.total = 3600 * tonumber(tz.offset.sign .. tz.offset.hour) + + -- use tonumber to check that tz.offset[min|sec] are not '' + if tonumber(tz.offset.min) then + tz.offset.total = tz.offset.total + 60 * tonumber(tz.offset.sign .. tz.offset.min) + end + if tonumber(tz.offset.sec) then + tz.offset.total = tz.offset.total + tonumber(tz.offset.sign .. tz.offset.sec) + end + + else + error('Not a valid POSIX TZ. "offset" invalid') + end + + + -- The rest of the POSIX fields are optional + if ( temp.afteroffset == '' ) then + -- we are finished. no more data. + return tz + end + + + -- DST -- If we are here, then "dst" field is present + + -- Parse out "dst" and validate + -- dst name comes after offset + tz.dst = {} + tz.dst.name, temp.afterdst = string.match(temp.afteroffset, '^(<[%w+-]+>)(.*)') + if not tz.dst.name then + -- try unquoted format of dst + tz.dst.name, temp.afterdst = string.match(temp.afteroffset, '^(%a+)(.*)') + end + if not tz.dst.name then + error('Not a valid POSIX TZ. "dst" present but not valid') + end + + -- tz.dst.name is defined, so process "rule" + -- Parse out specifications from "rule" + temp.dststart, temp.dststop = string.match(temp.afterdst, '^,([^,]+),([^,]+)') + if not temp.dststart then + error('Not a valid POSIX TZ. "dst" present but "rule" is not') + end + + + local function dstparse ( pos, spec ) + local t = {} + tz.dst[pos] = t + + + -- Handle explicit hour for DST change + t.hour = string.match(spec, '/(%d+)') + -- TODO: Implement explicit min and sec + + + local monkey = {} + monkey[1] = 31 -- January + monkey[2] = 28 + monkey[3] = 31 + monkey[4] = 30 + monkey[5] = 31 + monkey[6] = 30 + monkey[7] = 31 + monkey[8] = 31 + monkey[9] = 30 + monkey[10] = 31 + monkey[11] = 30 + monkey[12] = 31 + + local function whichmonth ( dayofyear ) + -- assume day = 1 for '1 Jan' + -- uses monkey to determine month + local month, dayofmonth + local remaindays = tonumber(dayofyear) + for i, n in ipairs(monkey) do + if remaindays > n then + -- Jnday is not in this month + remaindays = remaindays - n + else + month = i + dayofmonth = remaindays + break + end + end + t.month = month + t.day = dayofmonth + end + + -- Jn format (Julian day, with [1 <= n <= 365], no Feb 29) + t.Jnday = string.match(spec, '^J(%d+)') + if t.Jnday then + + whichmonth(t.Jnday) + + end + + -- n format (zero-based Julian day, [0 <= n <= 364/5], counting Feb 29) + t.nday = string.match(spec, '^(%d+)') + if t.nday then + + monkey[2] = 29 + whichmonth(t.nday + 1) + + end + + -- Mm.w.d (day d of week w of month m) + -- m is between 1 and 12 + -- w is between 1 and 5, where 1st,2nd..4th, and 5=last + -- d is between 0 (Sun) and 6 (Sat) + t.Mmonth, t.week, t.weekday = string.match(spec, 'M(%d+)%.(%d)%.(%d)') + if t.Mmonth then + t.month = t.Mmonth + end + + + -- Validation: t.month must be defined by now. + if not t.month then + error('Not a valid POSIX TZ. "rule" for DST "'..pos..'" is invalid') + end + end + + if temp.dststart and temp.dststop then + -- Parse and validate dst rule specifications. + dstparse('start', temp.dststart) + dstparse('stop', temp.dststop) + end + + + return tz + +end + -- cgit v1.2.3