------------------------------------------------------------------------------- -- POSIX TZ Parser for Lua -- Copyright (C) 2011 H. Andrew Latchman -- -- Performs some validation of TZ string -- based on fields needed 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 tonumber = tonumber local pairs = pairs local ipairs = ipairs local string = require('string') local core = require('posixtz.core') -- Rest of the code is part of module "posixtz" module('posixtz') from_file = core.from_file 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 -- dayofyear 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 -- TODO: determine if the desired year is a leap year 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