webmcp
view libraries/atom/atom.lua @ 64:3d43a5cf17c1
Compatibility with Lua 5.2
| author | jbe | 
|---|---|
| date | Sun Apr 15 16:04:33 2012 +0200 (2012-04-15) | 
| parents | 985024b16520 | 
| children | 24ed2cd053aa | 
 line source
     1 #!/usr/bin/env lua
     3 local _G             = _G
     4 local _VERSION       = _VERSION
     5 local assert         = assert
     6 local error          = error
     7 local getmetatable   = getmetatable
     8 local ipairs         = ipairs
     9 local next           = next
    10 local pairs          = pairs
    11 local print          = print
    12 local rawequal       = rawequal
    13 local rawget         = rawget
    14 local rawlen         = rawlen
    15 local rawset         = rawset
    16 local select         = select
    17 local setmetatable   = setmetatable
    18 local tonumber       = tonumber
    19 local tostring       = tostring
    20 local type           = type
    22 local math      = math
    23 local string    = string
    24 local table     = table
    26 local _M = {}
    27 if _ENV then
    28   _ENV = _M
    29 else
    30   _G[...] = _M
    31   setfenv(1, _M)
    32 end
    36 ---------------------------------------
    37 -- general functions and definitions --
    38 ---------------------------------------
    40 --[[--
    41 bool =            -- true, if value is an integer within resolution
    42 atom.is_integer(
    43   value           -- value to be tested
    44 )
    46 This function returns true if the given object is an integer within resolution.
    48 --]]--
    49 function is_integer(i)
    50   return
    51     type(i) == "number" and i % 1 == 0 and
    52     (i + 1) - i == 1 and i - (i - 1) == 1
    53 end
    54 --//--
    56 --[[--
    57 atom.not_a_number
    59 Value representing an invalid numeric result. Used for atom.integer.invalid and atom.number.invalid.
    61 --]]--
    62 not_a_number = 0 / 0
    63 --//--
    65 do
    67   local shadow = setmetatable({}, { __mode = "k" })
    69   local type_mt = { __index = {} }
    71   function type_mt:__call(...)
    72     return self:new(...)
    73   end
    75   function type_mt.__index:_create(data)
    76     local value = setmetatable({}, self)
    77     shadow[value] = data
    78     return value
    79   end
    81   local function write_prohibited()
    82     error("Modification of an atom is prohibited.")
    83   end
    85   -- returns a new type as a table, which serves also as metatable
    86   function create_new_type(name)
    87     local t = setmetatable(
    88       { methods = {}, getters = {}, name = name },
    89       type_mt
    90     )
    91     function t.__index(self, key)
    92       local data = shadow[self]
    93       local value = data[key]
    94       if value ~= nil then return value end
    95       local method = t.methods[key]
    96       if method then return method end
    97       local getter = t.getters[key]
    98       if getter then return getter(self) end
    99     end
   100     t.__newindex = write_prohibited
   101     return t
   102   end
   104   --[[--
   105   bool =          -- true, if 'value' is of type 't'
   106   atom.has_type(
   107     value,        -- any value
   108     t             -- atom time, e.g. atom.date, or lua type, e.g. "string"
   109   )
   111   This function checks, if a value is of a given type. The value may be an invalid value though, e.g. atom.date.invalid.
   113   --]]--
   114   function has_type(value, t)
   115     if t == nil then error("No type passed to has_type(...) function.") end
   116     local lua_type = type(value)
   117     return
   118       lua_type == t or
   119       getmetatable(value) == t or
   120       (lua_type == "boolean" and t == _M.boolean) or
   121       (lua_type == "string" and t == _M.string) or (
   122         lua_type == "number" and
   123         (t == _M.number or (
   124           t == _M.integer and (
   125             not (value <= 0 or value >= 0) or (
   126               value % 1 == 0 and
   127               (value + 1) - value == 1 and
   128               value - (value - 1) == 1
   129             )
   130           )
   131         ))
   132       )
   133   end
   134   --//--
   136   --[[--
   137   bool =          -- true, if 'value' is of type 't'
   138   atom.is_valid(
   139     value,        -- any value
   140     t             -- atom time, e.g. atom.date, or lua type, e.g. "string"
   141   )
   143   This function checks, if a value is valid. It optionally checks, if the value is of a given type.
   145   --]]--
   146   function is_valid(value, t)
   147     local lua_type = type(value)
   148     if lua_type == "table" then
   149       local mt = getmetatable(value)
   150       if t then
   151         return t == mt and not value.invalid
   152       else
   153         return (getmetatable(mt) == type_mt) and not value.invalid
   154       end
   155     elseif lua_type == "boolean" then
   156       return not t or t == "boolean" or t == _M.boolean
   157     elseif lua_type == "string" then
   158       return not t or t == "string" or t == _M.string
   159     elseif lua_type == "number" then
   160       if t == _M.integer then
   161         return
   162           value % 1 == 0 and
   163           (value + 1) - value == 1 and
   164           value - (value - 1) == 1
   165       else
   166         return
   167           (not t or t == "number" or t == _M.number) and
   168           (value <= 0 or value >= 0)
   169       end
   170     else
   171       return false
   172     end
   173   end
   174   --//--
   176 end
   178 --[[--
   179 string =    -- string representation to be passed to a load function
   180 atom.dump(
   181   value     -- value to be dumped
   182 )
   184 This function returns a string representation of the given value.
   186 --]]--
   187 function dump(obj)
   188   if obj == nil then
   189     return ""
   190   else
   191     return tostring(obj)
   192   end
   193 end
   194 --//--
   198 -------------
   199 -- boolean --
   200 -------------
   202 boolean = { name = "boolean" }
   204 --[[--
   205 bool =              -- true, false, or nil
   206 atom.boolean:load(
   207   string            -- string to be interpreted as boolean
   208 )
   210 This method returns true or false or nil, depending on the input string.
   212 --]]--
   213 function boolean:load(str)
   214   if str == nil or str == "" then
   215     return nil
   216   elseif type(str) ~= "string" then
   217     error("String expected")
   218   elseif string.find(str, "^[TtYy1]") then
   219     return true
   220   elseif string.find(str, "^[FfNn0]") then
   221     return false
   222   else
   223     return nil  -- we don't have an undefined bool
   224   end
   225 end
   226 --//--
   230 ------------
   231 -- string --
   232 ------------
   234 _M.string = { name = "string" }
   236 --[[--
   237 string =            -- the same string
   238 atom.string:load(
   239   string            -- a string
   240 )
   242 This method returns the passed string, or throws an error, if the passed argument is not a string.
   244 --]]--
   245 function _M.string:load(str)
   246   if str == nil then
   247     return nil
   248   elseif type(str) ~= "string" then
   249     error("String expected")
   250   else
   251     return str
   252   end
   253 end
   254 --//--
   258 -------------
   259 -- integer --
   260 -------------
   262 integer = { name = "integer" }
   264 --[[--
   265 int =              -- an integer or atom.integer.invalid (atom.not_a_number)
   266 atom.integer:load(
   267   string           -- a string representing an integer
   268 )
   270 This method returns an integer represented by the given string. If the string doesn't represent a valid integer, then not-a-number is returned.
   272 --]]--
   273 function integer:load(str)
   274   if str == nil or str == "" then
   275     return nil
   276   elseif type(str) ~= "string" then
   277     error("String expected")
   278   else
   279     local num = tonumber(str)
   280     if is_integer(num) then return num else return not_a_number end
   281   end
   282 end
   283 --//--
   285 --[[--
   286 atom.integer.invalid
   288 This represents an invalid integer.
   290 --]]--
   291 integer.invalid = not_a_number
   292 --//
   296 ------------
   297 -- number --
   298 ------------
   300 number = create_new_type("number")
   302 --[[--
   303 int =              -- a number or atom.number.invalid (atom.not_a_number)
   304 atom.number:load(
   305   string           -- a string representing a number
   306 )
   308 This method returns a number represented by the given string. If the string doesn't represent a valid number, then not-a-number is returned.
   310 --]]--
   311 function number:load(str)
   312   if str == nil or str == "" then
   313     return nil
   314   elseif type(str) ~= "string" then
   315     error("String expected")
   316   else
   317     return tonumber(str) or not_a_number
   318   end
   319 end
   320 --//--
   322 --[[--
   323 atom.number.invalid
   325 This represents an invalid number.
   327 --]]--
   328 number.invalid = not_a_number
   329 --//--
   333 --------------
   334 -- fraction --
   335 --------------
   337 fraction = create_new_type("fraction")
   339 --[[--
   340 i =        -- the greatest common divisor (GCD) of all given natural numbers
   341 atom.gcd(
   342   a,      -- a natural number
   343   b,      -- another natural number
   344   ...     -- optionally more natural numbers
   345 )
   347 This function returns the greatest common divisor (GCD) of two or more natural numbers.
   349 --]]--
   350 function gcd(a, b, ...)
   351   if a % 1 ~= 0 or a <= 0 then return 0 / 0 end
   352   if b == nil then
   353     return a
   354   else
   355     if b % 1 ~= 0 or b <= 0 then return 0 / 0 end
   356     if ... == nil then
   357       local k = 0
   358       local t
   359       while a % 2 == 0 and b % 2 == 0 do
   360         a = a / 2; b = b / 2; k = k + 1
   361       end
   362       if a % 2 == 0 then t = a else t = -b end
   363       while t ~= 0 do
   364         while t % 2 == 0 do t = t / 2 end
   365         if t > 0 then a = t else b = -t end
   366         t = a - b
   367       end
   368       return a * 2 ^ k
   369     else
   370       return gcd(gcd(a, b), ...)
   371     end
   372   end
   373 end
   374 --//--
   376 --[[--
   377 i =        --the least common multiple (LCD) of all given natural numbers
   378 atom.lcm(
   379   a,       -- a natural number
   380   b,       -- another natural number
   381   ...      -- optionally more natural numbers
   382 )
   384 This function returns the least common multiple (LCD) of two or more natural numbers.
   386 --]]--
   387 function lcm(a, b, ...)
   388   if a % 1 ~= 0 or a <= 0 then return 0 / 0 end
   389   if b == nil then
   390     return a
   391   else
   392     if b % 1 ~= 0 or b <= 0 then return 0 / 0 end
   393     if ... == nil then
   394       return a * b / gcd(a, b)
   395     else
   396       return lcm(lcm(a, b), ...)
   397     end
   398   end
   399 end
   400 --//--
   402 --[[--
   403 atom.fraction.invalid
   405 Value representing an invalid fraction.
   407 --]]--
   408 fraction.invalid = fraction:_create{
   409   numerator = not_a_number, denominator = not_a_number, invalid = true
   410 }
   411 --//--
   413 --[[--
   414 frac =              -- fraction
   415 atom.fraction:new(
   416   numerator,        -- numerator
   417   denominator       -- denominator
   418 )
   420 This method creates a new fraction.
   422 --]]--
   423 function fraction:new(numerator, denominator)
   424   if not (
   425     (numerator == nil   or type(numerator)   == "number") and
   426     (denominator == nil or type(denominator) == "number")
   427   ) then
   428     error("Invalid arguments passed to fraction constructor.")
   429   elseif
   430     (not is_integer(numerator)) or
   431     (denominator and (not is_integer(denominator)))
   432   then
   433     return fraction.invalid
   434   elseif denominator then
   435     if denominator == 0 then
   436       return fraction.invalid
   437     elseif numerator == 0 then
   438       return fraction:_create{ numerator = 0, denominator = 1, float = 0 }
   439     else
   440       local d = gcd(math.abs(numerator), math.abs(denominator))
   441       if denominator < 0 then d = -d end
   442       local numerator2, denominator2 = numerator / d, denominator / d
   443       return fraction:_create{
   444         numerator   = numerator2,
   445         denominator = denominator2,
   446         float       = numerator2 / denominator2
   447       }
   448     end
   449   else
   450     return fraction:_create{
   451       numerator = numerator, denominator = 1, float = numerator
   452     }
   453   end
   454 end
   455 --//--
   457 --[[--
   458 frac =               -- fraction represented by the given string
   459 atom.fraction:load(
   460   string             -- string representation of a fraction
   461 )
   463 This method returns a fraction represented by the given string.
   465 --]]--
   466 function fraction:load(str)
   467   if str == nil or str == "" then
   468     return nil
   469   elseif type(str) ~= "string" then
   470     error("String expected")
   471   else
   472     local sign, int = string.match(str, "^(%-?)([0-9]+)$")
   473     if sign == "" then return fraction:new(tonumber(int))
   474     elseif sign == "-" then return fraction:new(- tonumber(int))
   475     end
   476     local sign, n, d = string.match(str, "^(%-?)([0-9]+)/([0-9]+)$")
   477     if sign == "" then return fraction:new(tonumber(n), tonumber(d))
   478     elseif sign == "-" then return fraction:new(- tonumber(n), tonumber(d))
   479     end
   480     return fraction.invalid
   481   end
   482 end
   483 --//--
   485 function fraction:__tostring()
   486   if self.invalid then
   487     return "not_a_fraction"
   488   else
   489     return self.numerator .. "/" .. self.denominator
   490   end
   491 end
   493 function fraction.__eq(value1, value2)
   494   if value1.invalid or value2.invalid then
   495     return false
   496   else
   497     return
   498       value1.numerator == value2.numerator and
   499       value1.denominator == value2.denominator
   500   end
   501 end
   503 function fraction.__lt(value1, value2)
   504   if value1.invalid or value2.invalid then
   505     return false
   506   else
   507     return value1.float < value2.float
   508   end
   509 end
   511 function fraction.__le(value1, value2)
   512   if value1.invalid or value2.invalid then
   513     return false
   514   else
   515     return value1.float <= value2.float
   516   end
   517 end
   519 function fraction:__unm()
   520   return fraction(-self.numerator, self.denominator)
   521 end
   523 do
   525   local function extract(value1, value2)
   526     local n1, d1, n2, d2
   527     if getmetatable(value1) == fraction then
   528       n1 = value1.numerator
   529       d1 = value1.denominator
   530     elseif type(value1) == "number" then
   531       n1 = value1
   532       d1 = 1
   533     else
   534       error("Left operand of operator has wrong type.")
   535     end
   536     if getmetatable(value2) == fraction then
   537       n2 = value2.numerator
   538       d2 = value2.denominator
   539     elseif type(value2) == "number" then
   540       n2 = value2
   541       d2 = 1
   542     else
   543       error("Right operand of operator has wrong type.")
   544     end
   545     return n1, d1, n2, d2
   546   end
   548   function fraction.__add(value1, value2)
   549     local n1, d1, n2, d2 = extract(value1, value2)
   550     return fraction(n1 * d2 + n2 * d1, d1 * d2)
   551   end
   553   function fraction.__sub(value1, value2)
   554     local n1, d1, n2, d2 = extract(value1, value2)
   555     return fraction(n1 * d2 - n2 * d1, d1 * d2)
   556   end
   558   function fraction.__mul(value1, value2)
   559     local n1, d1, n2, d2 = extract(value1, value2)
   560     return fraction(n1 * n2, d1 * d2)
   561   end
   563   function fraction.__div(value1, value2)
   564     local n1, d1, n2, d2 = extract(value1, value2)
   565     return fraction(n1 * d2, d1 * n2)
   566   end
   568   function fraction.__pow(value1, value2)
   569     local n1, d1, n2, d2 = extract(value1, value2)
   570     local n1_abs = math.abs(n1)
   571     local d1_abs = math.abs(d1)
   572     local n2_abs = math.abs(n2)
   573     local d2_abs = math.abs(d2)
   574     local numerator, denominator
   575     if d2_abs == 1 then
   576       numerator = n1_abs
   577       denominator = d1_abs
   578     else
   579       numerator = 0
   580       while true do
   581         local t = numerator ^ d2_abs
   582         if t == n1_abs then break end
   583         if not (t < n1_abs) then return value1.float / value2.float end
   584         numerator = numerator + 1
   585       end
   586       denominator = 1
   587       while true do
   588         local t = denominator ^ d2_abs
   589         if t == d1_abs then break end
   590         if not (t < d1_abs) then return value1.float / value2.float end
   591         denominator = denominator + 1
   592       end
   593     end
   594     if n1 < 0 then
   595       if d2_abs % 2 == 1 then
   596         numerator = -numerator
   597       else
   598         return fraction.invalid
   599       end
   600     end
   601     if n2 < 0 then
   602       numerator, denominator = denominator, numerator
   603     end
   604     return fraction(numerator ^ n2_abs, denominator ^ n2_abs)
   605   end
   607 end
   611 ----------
   612 -- date --
   613 ----------
   615 date = create_new_type("date")
   617 do
   618   local c1 = 365             -- days of a non-leap year
   619   local c4 = 4 * c1 + 1      -- days of a full 4 year cycle
   620   local c100 = 25 * c4 - 1   -- days of a full 100 year cycle
   621   local c400 = 4 * c100 + 1  -- days of a full 400 year cycle
   622   local get_month_offset     -- function returning days elapsed within
   623                              -- the given year until the given month
   624                              -- (exclusive the given month)
   625   do
   626     local normal_month_offsets = {}
   627     local normal_month_lengths = {
   628       31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
   629     }
   630     local sum = 0
   631     for i = 1, 12 do
   632       normal_month_offsets[i] = sum
   633       sum = sum + normal_month_lengths[i]
   634     end
   635     function get_month_offset(year, month)
   636       if
   637         (((year % 4 == 0) and not (year % 100 == 0)) or (year % 400 == 0))
   638         and month > 2
   639       then
   640         return normal_month_offsets[month] + 1
   641       else
   642         return normal_month_offsets[month]
   643       end
   644     end
   645   end
   647   --[[--
   648   jd =                  -- days from January 1st 1970
   649   atom.date.ymd_to_jd(
   650     year,               -- year
   651     month,              -- month from 1 to 12
   652     day                 -- day from 1 to 31
   653   )
   655   This function calculates the days from January 1st 1970 for a given year, month and day.
   657   --]]--
   658   local offset = 0
   659   function date.ymd_to_jd(year, month, day)
   660     assert(is_integer(year), "Invalid year specified.")
   661     assert(is_integer(month), "Invalid month specified.")
   662     assert(is_integer(day), "Invalid day specified.")
   663     local calc_year = year - 1
   664     local n400 = math.floor(calc_year / 400)
   665     local r400 = calc_year % 400
   666     local n100 = math.floor(r400 / 100)
   667     local r100 = r400 % 100
   668     local n4 = math.floor(r100 / 4)
   669     local n1 = r100 % 4
   670     local jd = (
   671       c400 * n400 + c100 * n100 + c4 * n4 + c1 * n1 +
   672       get_month_offset(year, month) + (day - 1)
   673     )
   674     return jd - offset
   675   end
   676   offset = date.ymd_to_jd(1970, 1, 1)
   677   --//--
   679   --[[--
   680   year,                 -- year
   681   month,                -- month from 1 to 12
   682   day =                 -- day from 1 to 31
   683   atom.date.jd_to_ymd(
   684     jd,                 -- days from January 1st 1970
   685   )
   687   Given the days from January 1st 1970 this function returns year, month and day.
   689   --]]--
   690   function date.jd_to_ymd(jd)
   691     assert(is_integer(jd), "Invalid julian date specified.")
   692     local calc_jd = jd + offset
   693     assert(is_integer(calc_jd), "Julian date is out of range.")
   694     local n400 = math.floor(calc_jd / c400)
   695     local r400 = calc_jd % c400
   696     local n100 = math.floor(r400 / c100)
   697     local r100 = r400 % c100
   698     if n100 == 4 then n100, r100 = 3, c100 end
   699     local n4 = math.floor(r100 / c4)
   700     local r4 = r100 % c4
   701     local n1 = math.floor(r4 / c1)
   702     local r1 = r4 % c1
   703     if n1 == 4 then n1, r1 = 3, c1 end
   704     local year = 1 + 400 * n400 + 100 * n100 + 4 * n4 + n1
   705     local month = 1 + math.floor(r1 / 31)
   706     local month_offset = get_month_offset(year, month)
   707     if month < 12 then
   708       local next_month_offset = get_month_offset(year, month + 1)
   709       if r1 >= next_month_offset then
   710         month = month + 1
   711         month_offset = next_month_offset
   712       end
   713     end
   714     local day = 1 + r1 - month_offset
   715     return year, month, day
   716   end
   717   --//--
   718 end
   720 --[[--
   721 atom.date.invalid
   723 Value representing an invalid date.
   725 --]]--
   726 date.invalid = date:_create{
   727   jd = not_a_number,
   728   year = not_a_number, month = not_a_number, day = not_a_number,
   729   invalid = true
   730 }
   731 --//--
   733 --[[--
   734 d =                             -- date based on the given data
   735 atom.date:new{
   736   jd           = jd,            -- days since January 1st 1970
   737   year         = year,          -- year
   738   month        = month,         -- month from 1 to 12
   739   day          = day,           -- day from 1 to 31
   740   iso_weekyear = iso_weekyear,  -- year according to ISO 8601
   741   iso_week     = iso_week,      -- week number according to ISO 8601
   742   iso_weekday  = iso_weekday,   -- day of week from 1 for monday to 7 for sunday
   743   us_weekyear  = us_weekyear,   -- year
   744   us_week      = us_week,       -- week number according to US style counting
   745   us_weekday   = us_weekday     -- day of week from 1 for sunday to 7 for saturday
   746 }
   748 This method returns a new date value, based on given data.
   750 --]]--
   751 function date:new(args)
   752   local args = args
   753   if type(args) == "number" then args = { jd = args } end
   754   if type(args) == "table" then
   755     local year, month, day = args.year, args.month, args.day
   756     local jd = args.jd
   757     local iso_weekyear = args.iso_weekyear
   758     local iso_week     = args.iso_week
   759     local iso_weekday  = args.iso_weekday
   760     local us_week      = args.us_week
   761     local us_weekday   = args.us_weekday
   762     if
   763       type(year)  == "number" and
   764       type(month) == "number" and
   765       type(day)   == "number"
   766     then
   767       if
   768         is_integer(year)  and year >= 1  and year <= 9999 and
   769         is_integer(month) and month >= 1 and month <= 12  and
   770         is_integer(day)   and day >= 1   and day <= 31
   771       then
   772         return date:_create{
   773           jd = date.ymd_to_jd(year, month, day),
   774           year = year, month = month, day = day
   775         }
   776       else
   777         return date.invalid
   778       end
   779     elseif type(jd) == "number" then
   780       if is_integer(jd) and jd >= -719162 and jd <= 2932896 then
   781         local year, month, day = date.jd_to_ymd(jd)
   782         return date:_create{
   783           jd = jd, year = year, month = month, day = day
   784         }
   785       else
   786         return date.invalid
   787       end
   788     elseif
   789       type(year)        == "number" and not iso_weekyear and
   790       type(iso_week)    == "number" and
   791       type(iso_weekday) == "number"
   792     then
   793       if
   794         is_integer(year) and
   795         is_integer(iso_week)     and iso_week >= 0 and iso_week <= 53 and
   796         is_integer(iso_weekday)  and iso_weekday >= 1 and iso_weekday <= 7
   797       then
   798         local jan4 = date{ year = year, month = 1, day = 4 }
   799         local reference = jan4 - jan4.iso_weekday - 7  -- Sun. of week -1
   800         return date(reference + 7 * iso_week + iso_weekday)
   801       else
   802         return date.invalid
   803       end
   804     elseif
   805       type(iso_weekyear) == "number" and not year and
   806       type(iso_week)     == "number" and
   807       type(iso_weekday)  == "number"
   808     then
   809       if
   810         is_integer(iso_weekyear) and
   811         is_integer(iso_week)     and iso_week >= 0 and iso_week <= 53 and
   812         is_integer(iso_weekday)  and iso_weekday >= 1 and iso_weekday <= 7
   813       then
   814         local guessed = date{
   815           year        = iso_weekyear,
   816           iso_week    = iso_week,
   817           iso_weekday = iso_weekday
   818         }
   819         if guessed.invalid or guessed.iso_weekyear == iso_weekyear then
   820           return guessed
   821         else
   822           local year
   823           if iso_week <= 1 then
   824             year = iso_weekyear - 1
   825           elseif iso_week >= 52 then
   826             year = iso_weekyear + 1
   827           else
   828             error("Internal error in ISO week computation occured.")
   829           end
   830           return date{
   831             year = year, iso_week = iso_week, iso_weekday = iso_weekday
   832           }
   833         end
   834       else
   835         return date.invalid
   836       end
   837     elseif
   838       type(year) == "number" and
   839       type(us_week)     == "number" and
   840       type(us_weekday)  == "number"
   841     then
   842       if
   843         is_integer(year) and
   844         is_integer(us_week)     and us_week >= 0    and us_week <= 54   and
   845         is_integer(us_weekday)  and us_weekday >= 1 and us_weekday <= 7
   846       then
   847         local jan1 = date{ year = year, month = 1, day = 1 }
   848         local reference = jan1 - jan1.us_weekday - 7  -- Sat. of week -1
   849         return date(reference + 7 * us_week + us_weekday)
   850       else
   851         return date.invalid
   852       end
   853     end
   854   end
   855   error("Illegal arguments passed to date constructor.")
   856 end
   857 --//--
   859 --[[--
   860 atom.date:get_current()
   862 This function returns today's date.
   864 --]]--
   865 function date:get_current()
   866   local now = os.date("*t")
   867   return date{
   868     year = now.year, month = now.month, day = now.day
   869   }
   870 end
   871 --//--
   873 --[[--
   874 date =           -- date represented by the string
   875 atom.date:load(
   876   string         -- string representing a date
   877 )
   879 This method returns a date represented by the given string.
   881 --]]--
   882 function date:load(str)
   883   if str == nil or str == "" then
   884     return nil
   885   elseif type(str) ~= "string" then
   886     error("String expected")
   887   else
   888     local year, month, day = string.match(
   889       str, "^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"
   890     )
   891     if year then
   892       return date{
   893         year = tonumber(year),
   894         month = tonumber(month),
   895         day = tonumber(day)
   896       }
   897     else
   898       return date.invalid
   899     end
   900   end
   901 end
   902 --//--
   904 function date:__tostring()
   905   if self.invalid then
   906     return "invalid_date"
   907   else
   908     return string.format(
   909       "%04i-%02i-%02i", self.year, self.month, self.day
   910     )
   911   end
   912 end
   914 function date.getters:midnight()
   915   return time{ year = self.year, month = self.month, day = self.day }
   916 end
   918 function date.getters:midday()
   919   return time{
   920     year = self.year, month = self.month, day = self.day,
   921     hour = 12
   922   }
   923 end
   925 function date.getters:iso_weekday()  -- 1 = Monday
   926   return (self.jd + 3) % 7 + 1
   927 end
   929 function date.getters:us_weekday()  -- 1 = Sunday
   930   return (self.jd + 4) % 7 + 1
   931 end
   933 function date.getters:iso_weekyear()  -- ISO week-numbering year
   934   local year, month, day = self.year, self.month, self.day
   935   local iso_weekday      = self.iso_weekday
   936   if month == 1 then
   937     if
   938       (day == 3 and iso_weekday == 7) or
   939       (day == 2 and iso_weekday >= 6) or
   940       (day == 1 and iso_weekday >= 5)
   941     then
   942       return year - 1
   943     end
   944   elseif month == 12 then
   945     if
   946       (day == 29 and iso_weekday == 1) or
   947       (day == 30 and iso_weekday <= 2) or
   948       (day == 31 and iso_weekday <= 3)
   949     then
   950       return year + 1
   951     end
   952   end
   953   return year
   954 end
   956 function date.getters:iso_week()
   957   local jan4 = date{ year = self.iso_weekyear, month = 1, day = 4 }
   958   local reference = jan4.jd - jan4.iso_weekday - 6  -- monday of week 0
   959   return math.floor((self.jd - reference) / 7)
   960 end
   962 function date.getters:us_week()
   963   local jan1 = date{ year = self.year, month = 1, day = 1 }
   964   local reference = jan1.jd - jan1.us_weekday - 6  -- sunday of week 0
   965   return math.floor((self.jd - reference) / 7)
   966 end
   968 function date.__eq(value1, value2)
   969   if value1.invalid or value2.invalid then
   970     return false
   971   else
   972     return value1.jd == value2.jd
   973   end
   974 end
   976 function date.__lt(value1, value2)
   977   if value1.invalid or value2.invalid then
   978     return false
   979   else
   980     return value1.jd < value2.jd
   981   end
   982 end
   984 function date.__le(value1, value2)
   985   if value1.invalid or value2.invalid then
   986     return false
   987   else
   988     return value1.jd <= value2.jd
   989   end
   990 end
   992 function date.__add(value1, value2)
   993   if getmetatable(value1) == date then
   994     if getmetatable(value2) == date then
   995       error("Can not add two dates.")
   996     elseif type(value2) == "number" then
   997       return date(value1.jd + value2)
   998     else
   999       error("Right operand of '+' operator has wrong type.")
  1000     end
  1001   elseif type(value1) == "number" then
  1002     if getmetatable(value2) == date then
  1003       return date(value1 + value2.jd)
  1004     else
  1005       error("Assertion failed")
  1006     end
  1007   else
  1008     error("Left operand of '+' operator has wrong type.")
  1009   end
  1010 end
  1012 function date.__sub(value1, value2)
  1013   if not getmetatable(value1) == date then
  1014     error("Left operand of '-' operator has wrong type.")
  1015   end
  1016   if getmetatable(value2) == date then
  1017     return value1.jd - value2.jd  -- TODO: transform to interval
  1018   elseif type(value2) == "number" then
  1019     return date(value1.jd - value2)
  1020   else
  1021     error("Right operand of '-' operator has wrong type.")
  1022   end
  1023 end
  1027 ---------------
  1028 -- timestamp --
  1029 ---------------
  1031 timestamp = create_new_type("timestamp")
  1033 --[[--
  1034 tsec =                          -- seconds since January 1st 1970 00:00
  1035 atom.timestamp.ymdhms_to_tsec(
  1036   year,                         -- year
  1037   month,                        -- month from 1 to 12
  1038   day,                          -- day from 1 to 31
  1039   hour,                         -- hour from 0 to 23
  1040   minute,                       -- minute from 0 to 59
  1041   second                        -- second from 0 to 59
  1042 )
  1044 Given the year, month, day, hour, minute and second, this function returns the number of seconds since January 1st 1970 00:00.
  1046 --]]--
  1047 function timestamp.ymdhms_to_tsec(year, month, day, hour, minute, second)
  1048   return
  1049     86400 * date.ymd_to_jd(year, month, day) +
  1050     3600 * hour + 60 * minute + second
  1051 end
  1052 --//--
  1054 --[[--
  1055 year,                      -- year
  1056 month,                     -- month from 1 to 12
  1057 day,                       -- day from 1 to 31
  1058 hour,                      -- hour from 0 to 23
  1059 minute,                    -- minute from 0 to 59
  1060 second =                   -- second from 0 to 59
  1061 atom.timestamp.tsec_to_ymdhms(
  1062   tsec                     -- seconds since January 1st 1970 00:00
  1063 )
  1065 Given the seconds since January 1st 1970 00:00, this function returns the year, month, day, hour, minute and second.
  1067 --]]--
  1068 function timestamp.tsec_to_ymdhms(tsec)
  1069   local jd   = math.floor(tsec / 86400)
  1070   local dsec = tsec % 86400
  1071   local year, month, day = date.jd_to_ymd(jd)
  1072   local hour   = math.floor(dsec / 3600)
  1073   local minute = math.floor((dsec % 3600) / 60)
  1074   local second = dsec % 60
  1075   return year, month, day, hour, minute, second
  1076 end
  1077 --//--
  1079 --[[--
  1080 timestamp.invalid
  1082 Value representing an invalid timestamp.
  1084 --]]--
  1085 timestamp.invalid = timestamp:_create{
  1086   tsec = not_a_number,
  1087   year = not_a_number, month = not_a_number, day = not_a_number,
  1088   hour = not_a_number, minute = not_a_number, second = not_a_number,
  1089   invalid = true
  1090 }
  1091 --//--
  1093 --[[--
  1094 ts =                 -- timestamp based on given data
  1095 atom.timestamp:new{
  1096   tsec   = tsec,     -- seconds since January 1st 1970 00:00
  1097   year   = year,     -- year
  1098   month  = month,    -- month from 1 to 12
  1099   day    = day,      -- day from 1 to 31
  1100   hour   = hour,     -- hour from 0 to 23
  1101   minute = minute,   -- minute from 0 to 59
  1102   second = second    -- second from 0 to 59
  1103 }
  1105 This method returns a new timestamp value, based on given data.
  1107 --]]--
  1108 function timestamp:new(args)
  1109   local args = args
  1110   if type(args) == "number" then args = { tsec = args } end
  1111   if type(args) == "table" then
  1112     if not args.second then
  1113       args.second = 0
  1114       if not args.minute then
  1115         args.minute = 0
  1116         if not args.hour then
  1117           args.hour = 0
  1118         end
  1119       end
  1120     end
  1121     if
  1122       type(args.year)   == "number" and
  1123       type(args.month)  == "number" and
  1124       type(args.day)    == "number" and
  1125       type(args.hour)   == "number" and
  1126       type(args.minute) == "number" and
  1127       type(args.second) == "number"
  1128     then
  1129       if
  1130         is_integer(args.year) and
  1131         args.year >= 1 and args.year <= 9999 and
  1132         is_integer(args.month) and
  1133         args.month >= 1 and args.month <= 12 and
  1134         is_integer(args.day) and
  1135         args.day >= 1 and args.day <= 31 and
  1136         is_integer(args.hour) and
  1137         args.hour >= 0 and args.hour <= 23 and
  1138         is_integer(args.minute) and
  1139         args.minute >= 0 and args.minute <= 59 and
  1140         is_integer(args.second) and
  1141         args.second >= 0 and args.second <= 59
  1142       then
  1143         return timestamp:_create{
  1144           tsec = timestamp.ymdhms_to_tsec(
  1145             args.year, args.month, args.day,
  1146             args.hour, args.minute, args.second
  1147           ),
  1148           year   = args.year,
  1149           month  = args.month,
  1150           day    = args.day,
  1151           hour   = args.hour,
  1152           minute = args.minute,
  1153           second = args.second
  1154         }
  1155       else
  1156         return timestamp.invalid
  1157       end
  1158     elseif type(args.tsec) == "number" then
  1159       if
  1160         is_integer(args.tsec) and
  1161         args.tsec >= -62135596800 and args.tsec <= 253402300799
  1162       then
  1163         local year, month, day, hour, minute, second =
  1164           timestamp.tsec_to_ymdhms(args.tsec)
  1165         return timestamp:_create{
  1166           tsec = args.tsec,
  1167           year = year, month = month, day = day,
  1168           hour = hour, minute = minute, second = second
  1169         }
  1170       else
  1171         return timestamp.invalid
  1172       end
  1173     end
  1174   end
  1175   error("Invalid arguments passed to timestamp constructor.")
  1176 end
  1177 --//--
  1179 --[[--
  1180 ts =                          -- current date/time as timestamp
  1181 atom.timestamp:get_current()
  1183 This function returns the current date and time as timestamp.
  1185 --]]--
  1186 function timestamp:get_current()
  1187   local now = os.date("*t")
  1188   return timestamp{
  1189     year = now.year, month = now.month, day = now.day,
  1190     hour = now.hour, minute = now.min, second = now.sec
  1191   }
  1192 end
  1193 --//--
  1195 --[[--
  1196 ts =             -- timestamp represented by the string
  1197 atom.timestamp:load(
  1198   string         -- string representing a timestamp
  1199 )
  1201 This method returns a timestamp represented by the given string.
  1203 --]]--
  1204 function timestamp:load(str)
  1205   if str == nil or str == "" then
  1206     return nil
  1207   elseif type(str) ~= "string" then
  1208     error("String expected")
  1209   else
  1210     local year, month, day, hour, minute, second = string.match(
  1211       str,
  1212       "^([0-9][0-9][0-9][0-9])%-([0-9][0-9])%-([0-9][0-9]) ([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$"
  1213     )
  1214     if year then
  1215       return timestamp{
  1216         year   = tonumber(year),
  1217         month  = tonumber(month),
  1218         day    = tonumber(day),
  1219         hour   = tonumber(hour),
  1220         minute = tonumber(minute),
  1221         second = tonumber(second)
  1222       }
  1223     else
  1224       return timestamp.invalid
  1225     end
  1226   end
  1227 end
  1229 function timestamp:__tostring()
  1230   if self.invalid then
  1231     return "invalid_timestamp"
  1232   else
  1233     return string.format(
  1234       "%04i-%02i-%02i %02i:%02i:%02i",
  1235       self.year, self.month, self.day, self.hour, self.minute, self.second
  1236     )
  1237   end
  1238 end
  1240 function timestamp.getters:date()
  1241   return date{ year = self.year, month = self.month, day = self.day }
  1242 end
  1244 function timestamp.getters:time()
  1245   return time{
  1246     hour = self.hour,
  1247     minute = self.minute,
  1248     second = self.second
  1249   }
  1250 end
  1252 function timestamp.__eq(value1, value2)
  1253   if value1.invalid or value2.invalid then
  1254     return false
  1255   else
  1256     return value1.tsec == value2.tsec
  1257   end
  1258 end
  1260 function timestamp.__lt(value1, value2)
  1261   if value1.invalid or value2.invalid then
  1262     return false
  1263   else
  1264     return value1.tsec < value2.tsec
  1265   end
  1266 end
  1268 function timestamp.__le(value1, value2)
  1269   if value1.invalid or value2.invalid then
  1270     return false
  1271   else
  1272     return value1.tsec <= value2.tsec
  1273   end
  1274 end
  1276 function timestamp.__add(value1, value2)
  1277   if getmetatable(value1) == timestamp then
  1278     if getmetatable(value2) == timestamp then
  1279       error("Can not add two timestamps.")
  1280     elseif type(value2) == "number" then
  1281       return timestamp(value1.tsec + value2)
  1282     else
  1283       error("Right operand of '+' operator has wrong type.")
  1284     end
  1285   elseif type(value1) == "number" then
  1286     if getmetatable(value2) == timestamp then
  1287       return timestamp(value1 + value2.tsec)
  1288     else
  1289       error("Assertion failed")
  1290     end
  1291   else
  1292     error("Left operand of '+' operator has wrong type.")
  1293   end
  1294 end
  1296 function timestamp.__sub(value1, value2)
  1297   if not getmetatable(value1) == timestamp then
  1298     error("Left operand of '-' operator has wrong type.")
  1299   end
  1300   if getmetatable(value2) == timestamp then
  1301     return value1.tsec - value2.tsec  -- TODO: transform to interval
  1302   elseif type(value2) == "number" then
  1303     return timestamp(value1.tsec - value2)
  1304   else
  1305     error("Right operand of '-' operator has wrong type.")
  1306   end
  1307 end
  1311 ----------
  1312 -- time --
  1313 ----------
  1315 time = create_new_type("time")
  1317 function time.hms_to_dsec(hour, minute, second)
  1318   return 3600 * hour + 60 * minute + second
  1319 end
  1321 function time.dsec_to_hms(dsec)
  1322   local hour   = math.floor(dsec / 3600)
  1323   local minute = math.floor((dsec % 3600) / 60)
  1324   local second = dsec % 60
  1325   return hour, minute, second
  1326 end
  1328 --[[--
  1329 atom.time.invalid
  1331 Value representing an invalid time of day.
  1333 --]]--
  1334 time.invalid = time:_create{
  1335   dsec = not_a_number,
  1336   hour = not_a_number, minute = not_a_number, second = not_a_number,
  1337   invalid = true
  1338 }
  1339 --//--
  1341 --[[--
  1342 t =                 -- time based on given data
  1343 atom.time:new{
  1344   dsec = dsec,      -- seconds since 00:00:00
  1345   hour = hour,      -- hour from 0 to 23
  1346   minute = minute,  -- minute from 0 to 59
  1347   second = second   -- second from 0 to 59
  1348 }
  1350 This method returns a new time value, based on given data.
  1352 --]]--
  1353 function time:new(args)
  1354   local args = args
  1355   if type(args) == "number" then args = { dsec = args } end
  1356   if type(args) == "table" then
  1357     if not args.second then
  1358       args.second = 0
  1359       if not args.minute then
  1360         args.minute = 0
  1361       end
  1362     end
  1363     if
  1364       type(args.hour)   == "number" and
  1365       type(args.minute) == "number" and
  1366       type(args.second) == "number"
  1367     then
  1368       if
  1369         is_integer(args.hour) and
  1370         args.hour >= 0 and args.hour <= 23 and
  1371         is_integer(args.minute) and
  1372         args.minute >= 0 and args.minute <= 59 and
  1373         is_integer(args.second) and
  1374         args.second >= 0 and args.second <= 59
  1375       then
  1376         return time:_create{
  1377           dsec = time.hms_to_dsec(args.hour, args.minute, args.second),
  1378           hour   = args.hour,
  1379           minute = args.minute,
  1380           second = args.second
  1381         }
  1382       else
  1383         return time.invalid
  1384       end
  1385     elseif type(args.dsec) == "number" then
  1386       if
  1387         is_integer(args.dsec) and
  1388         args.dsec >= 0 and args.dsec <= 86399
  1389       then
  1390         local hour, minute, second =
  1391           time.dsec_to_hms(args.dsec)
  1392         return time:_create{
  1393           dsec = args.dsec,
  1394           hour = hour, minute = minute, second = second
  1395         }
  1396       else
  1397         return time.invalid
  1398       end
  1399     end
  1400   end
  1401   error("Invalid arguments passed to time constructor.")
  1402 end
  1403 --//--
  1405 --[[--
  1406 t =                      -- current time of day
  1407 atom.time:get_current()
  1409 This method returns the current time of the day.
  1411 --]]--
  1412 function time:get_current()
  1413   local now = os.date("*t")
  1414   return time{ hour = now.hour, minute = now.min, second = now.sec }
  1415 end
  1416 --//--
  1418 --[[--
  1419 t =              -- time represented by the string
  1420 atom.time:load(
  1421   string         -- string representing a time of day
  1422 )
  1424 This method returns a time represented by the given string.
  1426 --]]--
  1427 function time:load(str)
  1428   if str == nil or str == "" then
  1429     return nil
  1430   elseif type(str) ~= "string" then
  1431     error("String expected")
  1432   else
  1433     local hour, minute, second = string.match(
  1434       str,
  1435       "^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$"
  1436     )
  1437     if hour then
  1438       return time{
  1439         hour   = tonumber(hour),
  1440         minute = tonumber(minute),
  1441         second = tonumber(second)
  1442       }
  1443     else
  1444       return time.invalid
  1445     end
  1446   end
  1447 end
  1448 --//--
  1450 function time:__tostring()
  1451   if self.invalid then
  1452     return "invalid_time"
  1453   else
  1454     return string.format(
  1455       "%02i:%02i:%02i",
  1456       self.hour, self.minute, self.second
  1457     )
  1458   end
  1459 end
  1461 function time.__eq(value1, value2)
  1462   if value1.invalid or value2.invalid then
  1463     return false
  1464   else
  1465     return value1.dsec == value2.dsec
  1466   end
  1467 end
  1469 function time.__lt(value1, value2)
  1470   if value1.invalid or value2.invalid then
  1471     return false
  1472   else
  1473     return value1.dsec < value2.dsec
  1474   end
  1475 end
  1477 function time.__le(value1, value2)
  1478   if value1.invalid or value2.invalid then
  1479     return false
  1480   else
  1481     return value1.dsec <= value2.dsec
  1482   end
  1483 end
  1485 function time.__add(value1, value2)
  1486   if getmetatable(value1) == time then
  1487     if getmetatable(value2) == time then
  1488       error("Can not add two times.")
  1489     elseif type(value2) == "number" then
  1490       return time((value1.dsec + value2) % 86400)
  1491     else
  1492       error("Right operand of '+' operator has wrong type.")
  1493     end
  1494   elseif type(value1) == "number" then
  1495     if getmetatable(value2) == time then
  1496       return time((value1 + value2.dsec) % 86400)
  1497     else
  1498       error("Assertion failed")
  1499     end
  1500   else
  1501     error("Left operand of '+' operator has wrong type.")
  1502   end
  1503 end
  1505 function time.__sub(value1, value2)
  1506   if not getmetatable(value1) == time then
  1507     error("Left operand of '-' operator has wrong type.")
  1508   end
  1509   if getmetatable(value2) == time then
  1510     return value1.dsec - value2.dsec  -- TODO: transform to interval
  1511   elseif type(value2) == "number" then
  1512     return time((value1.dsec - value2) % 86400)
  1513   else
  1514     error("Right operand of '-' operator has wrong type.")
  1515   end
  1516 end
  1518 function time.__concat(value1, value2)
  1519   local mt1, mt2 = getmetatable(value1), getmetatable(value2)
  1520   if mt1 == date and mt2 == time then
  1521     return timestamp{
  1522       year = value1.year, month = value1.month, day = value1.day,
  1523       hour = value2.hour, minute = value2.minute, second = value2.second
  1524     }
  1525   elseif mt1 == time and mt2 == date then
  1526     return timestamp{
  1527       year = value2.year, month = value2.month, day = value2.day,
  1528       hour = value1.hour, minute = value1.minute, second = value1.second
  1529     }
  1530   elseif mt1 == time then
  1531     error("Right operand of '..' operator has wrong type.")
  1532   elseif mt2 == time then
  1533     error("Left operand of '..' operator has wrong type.")
  1534   else
  1535     error("Assertion failed")
  1536   end
  1537 end
  1541 return _M
