summaryrefslogtreecommitdiffstats
path: root/posixtz.lua
blob: 74d5d9821117e18ae07f86706600433a067d203b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
-------------------------------------------------------------------------------
-- POSIX TZ Parser for Lua
-- Copyright (C) 2011 H. Andrew Latchman <aldevel@jamailca.com>
--
-- 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' '<GMT+5>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
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
					-- 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