webmcp
view libraries/atom/atom.lua @ 542:99a0f1165ef8
Use <db_class>:get_reference(...) instead of <db_class>.references[...]
| author | jbe | 
|---|---|
| date | Sat Oct 19 17:59:28 2019 +0200 (2019-10-19) | 
| parents | b19a6b4f61f3 | 
| children | 
 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 os        = os
    24 local string    = string
    25 local table     = table
    27 local _M = {}
    28 if _ENV then
    29   _ENV = _M
    30 else
    31   _G[...] = _M
    32   setfenv(1, _M)
    33 end
    37 ---------------------------------------
    38 -- general functions and definitions --
    39 ---------------------------------------
    41 --[[--
    42 bool =            -- true, if value is an integer within resolution
    43 atom.is_integer(
    44   value           -- value to be tested
    45 )
    47 This function returns true if the given object is an integer within resolution.
    49 --]]--
    50 function is_integer(i)
    51   return
    52     type(i) == "number" and i % 1 == 0 and
    53     (i + 1) - i == 1 and i - (i - 1) == 1
    54 end
    55 --//--
    57 --[[--
    58 atom.not_a_number
    60 Value representing an invalid numeric result. Used for atom.integer.invalid and atom.number.invalid.
    62 --]]--
    63 not_a_number = 0 / 0
    64 --//--
    66 do
    68   local shadow = setmetatable({}, { __mode = "k" })
    70   local type_mt = { __index = {} }
    72   function type_mt:__call(...)
    73     return self:new(...)
    74   end
    76   function type_mt.__index:_create(data)
    77     local value = setmetatable({}, self)
    78     shadow[value] = data
    79     return value
    80   end
    82   local function write_prohibited()
    83     error("Modification of an atom is prohibited.")
    84   end
    86   -- returns a new type as a table, which serves also as metatable
    87   function create_new_type(name)
    88     local t = setmetatable(
    89       { methods = {}, getters = {}, name = name },
    90       type_mt
    91     )
    92     function t.__index(self, key)
    93       local data = shadow[self]
    94       local value = data[key]
    95       if value ~= nil then return value end
    96       local method = t.methods[key]
    97       if method then return method end
    98       local getter = t.getters[key]
    99       if getter then return getter(self) end
   100     end
   101     t.__newindex = write_prohibited
   102     return t
   103   end
   105   --[[--
   106   bool =          -- true, if 'value' is of type 't'
   107   atom.has_type(
   108     value,        -- any value
   109     t             -- atom time, e.g. atom.date, or lua type, e.g. "string"
   110   )
   112   This function checks, if a value is of a given type. The value may be an invalid value though, e.g. atom.date.invalid.
   114   --]]--
   115   function has_type(value, t)
   116     if t == nil then error("No type passed to has_type(...) function.") end
   117     local lua_type = type(value)
   118     return
   119       lua_type == t or
   120       getmetatable(value) == t or
   121       (lua_type == "boolean" and t == _M.boolean) or
   122       (lua_type == "string" and t == _M.string) or (
   123         lua_type == "number" and
   124         (t == _M.number or (
   125           t == _M.integer and (
   126             not (value <= 0 or value >= 0) or (
   127               value % 1 == 0 and
   128               (value + 1) - value == 1 and
   129               value - (value - 1) == 1
   130             )
   131           )
   132         ))
   133       )
   134   end
   135   --//--
   137   --[[--
   138   bool =          -- true, if 'value' is of type 't'
   139   atom.is_valid(
   140     value,        -- any value
   141     t             -- atom time, e.g. atom.date, or lua type, e.g. "string"
   142   )
   144   This function checks, if a value is valid. It optionally checks, if the value is of a given type.
   146   --]]--
   147   function is_valid(value, t)
   148     local lua_type = type(value)
   149     if lua_type == "table" then
   150       local mt = getmetatable(value)
   151       if t then
   152         return t == mt and not value.invalid
   153       else
   154         return (getmetatable(mt) == type_mt) and not value.invalid
   155       end
   156     elseif lua_type == "boolean" then
   157       return not t or t == "boolean" or t == _M.boolean
   158     elseif lua_type == "string" then
   159       return not t or t == "string" or t == _M.string
   160     elseif lua_type == "number" then
   161       if t == _M.integer then
   162         return
   163           value % 1 == 0 and
   164           (value + 1) - value == 1 and
   165           value - (value - 1) == 1
   166       else
   167         return
   168           (not t or t == "number" or t == _M.number) and
   169           (value <= 0 or value >= 0)
   170       end
   171     else
   172       return false
   173     end
   174   end
   175   --//--
   177 end
   179 --[[--
   180 string =    -- string representation to be passed to a load function
   181 atom.dump(
   182   value     -- value to be dumped
   183 )
   185 This function returns a string representation of the given value.
   187 --]]--
   188 function dump(obj)
   189   if obj == nil then
   190     return ""
   191   else
   192     return tostring(obj)
   193   end
   194 end
   195 --//--
   199 -------------
   200 -- boolean --
   201 -------------
   203 boolean = { name = "boolean" }
   205 --[[--
   206 bool =              -- true, false, or nil
   207 atom.boolean:load(
   208   string            -- string to be interpreted as boolean
   209 )
   211 This method returns true or false or nil, depending on the input string.
   213 --]]--
   214 function boolean:load(str)
   215   if str == nil or str == "" then
   216     return nil
   217   elseif type(str) ~= "string" then
   218     error("String expected")
   219   elseif string.find(str, "^[TtYy1]") then
   220     return true
   221   elseif string.find(str, "^[FfNn0]") then
   222     return false
   223   else
   224     return nil  -- we don't have an undefined bool
   225   end
   226 end
   227 --//--
   231 ------------
   232 -- string --
   233 ------------
   235 _M.string = { name = "string" }
   237 --[[--
   238 string =            -- the same string
   239 atom.string:load(
   240   string            -- a string
   241 )
   243 This method returns the passed string, or throws an error, if the passed argument is not a string.
   245 --]]--
   246 function _M.string:load(str)
   247   if str == nil then
   248     return nil
   249   elseif type(str) ~= "string" then
   250     error("String expected")
   251   else
   252     return str
   253   end
   254 end
   255 --//--
   259 -------------
   260 -- integer --
   261 -------------
   263 integer = { name = "integer" }
   265 --[[--
   266 int =              -- an integer or atom.integer.invalid (atom.not_a_number)
   267 atom.integer:load(
   268   string           -- a string representing an integer
   269 )
   271 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.
   273 --]]--
   274 function integer:load(str)
   275   if str == nil or str == "" then
   276     return nil
   277   elseif type(str) ~= "string" then
   278     error("String expected")
   279   else
   280     local num = tonumber(str)
   281     if is_integer(num) then return num else return not_a_number end
   282   end
   283 end
   284 --//--
   286 --[[--
   287 atom.integer.invalid
   289 This represents an invalid integer.
   291 --]]--
   292 integer.invalid = not_a_number
   293 --//--
   297 ------------
   298 -- number --
   299 ------------
   301 number = create_new_type("number")
   303 --[[--
   304 int =              -- a number or atom.number.invalid (atom.not_a_number)
   305 atom.number:load(
   306   string           -- a string representing a number
   307 )
   309 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.
   311 --]]--
   312 function number:load(str)
   313   if str == nil or str == "" then
   314     return nil
   315   elseif type(str) ~= "string" then
   316     error("String expected")
   317   else
   318     return tonumber(str) or not_a_number
   319   end
   320 end
   321 --//--
   323 --[[--
   324 atom.number.invalid
   326 This represents an invalid number.
   328 --]]--
   329 number.invalid = not_a_number
   330 --//--
   334 --------------
   335 -- fraction --
   336 --------------
   338 fraction = create_new_type("fraction")
   340 --[[--
   341 i =        -- the greatest common divisor (GCD) of all given natural numbers
   342 atom.gcd(
   343   a,      -- a natural number
   344   b,      -- another natural number
   345   ...     -- optionally more natural numbers
   346 )
   348 This function returns the greatest common divisor (GCD) of two or more natural numbers.
   350 --]]--
   351 function gcd(a, b, ...)
   352   if a % 1 ~= 0 or a <= 0 then return 0 / 0 end
   353   if b == nil then
   354     return a
   355   else
   356     if b % 1 ~= 0 or b <= 0 then return 0 / 0 end
   357     if ... == nil then
   358       local k = 0
   359       local t
   360       while a % 2 == 0 and b % 2 == 0 do
   361         a = a / 2; b = b / 2; k = k + 1
   362       end
   363       if a % 2 == 0 then t = a else t = -b end
   364       while t ~= 0 do
   365         while t % 2 == 0 do t = t / 2 end
   366         if t > 0 then a = t else b = -t end
   367         t = a - b
   368       end
   369       return a * 2 ^ k
   370     else
   371       return gcd(gcd(a, b), ...)
   372     end
   373   end
   374 end
   375 --//--
   377 --[[--
   378 i =        --the least common multiple (LCD) of all given natural numbers
   379 atom.lcm(
   380   a,       -- a natural number
   381   b,       -- another natural number
   382   ...      -- optionally more natural numbers
   383 )
   385 This function returns the least common multiple (LCD) of two or more natural numbers.
   387 --]]--
   388 function lcm(a, b, ...)
   389   if a % 1 ~= 0 or a <= 0 then return 0 / 0 end
   390   if b == nil then
   391     return a
   392   else
   393     if b % 1 ~= 0 or b <= 0 then return 0 / 0 end
   394     if ... == nil then
   395       return a * b / gcd(a, b)
   396     else
   397       return lcm(lcm(a, b), ...)
   398     end
   399   end
   400 end
   401 --//--
   403 --[[--
   404 atom.fraction.invalid
   406 Value representing an invalid fraction.
   408 --]]--
   409 fraction.invalid = fraction:_create{
   410   numerator = not_a_number, denominator = not_a_number, invalid = true
   411 }
   412 --//--
   414 --[[--
   415 frac =              -- fraction
   416 atom.fraction:new(
   417   numerator,        -- numerator
   418   denominator       -- denominator
   419 )
   421 This method creates a new fraction.
   423 --]]--
   424 function fraction:new(numerator, denominator)
   425   if not (
   426     (numerator == nil   or type(numerator)   == "number") and
   427     (denominator == nil or type(denominator) == "number")
   428   ) then
   429     error("Invalid arguments passed to fraction constructor.")
   430   elseif
   431     (not is_integer(numerator)) or
   432     (denominator and (not is_integer(denominator)))
   433   then
   434     return fraction.invalid
   435   elseif denominator then
   436     if denominator == 0 then
   437       return fraction.invalid
   438     elseif numerator == 0 then
   439       return fraction:_create{ numerator = 0, denominator = 1, float = 0 }
   440     else
   441       local d = gcd(math.abs(numerator), math.abs(denominator))
   442       if denominator < 0 then d = -d end
   443       local numerator2, denominator2 = numerator / d, denominator / d
   444       return fraction:_create{
   445         numerator   = numerator2,
   446         denominator = denominator2,
   447         float       = numerator2 / denominator2
   448       }
   449     end
   450   else
   451     return fraction:_create{
   452       numerator = numerator, denominator = 1, float = numerator
   453     }
   454   end
   455 end
   456 --//--
   458 --[[--
   459 frac =               -- fraction represented by the given string
   460 atom.fraction:load(
   461   string             -- string representation of a fraction
   462 )
   464 This method returns a fraction represented by the given string.
   466 --]]--
   467 function fraction:load(str)
   468   if str == nil or str == "" then
   469     return nil
   470   elseif type(str) ~= "string" then
   471     error("String expected")
   472   else
   473     local sign, int = string.match(str, "^(%-?)([0-9]+)$")
   474     if sign == "" then return fraction:new(tonumber(int))
   475     elseif sign == "-" then return fraction:new(- tonumber(int))
   476     end
   477     local sign, n, d = string.match(str, "^(%-?)([0-9]+)/([0-9]+)$")
   478     if sign == "" then return fraction:new(tonumber(n), tonumber(d))
   479     elseif sign == "-" then return fraction:new(- tonumber(n), tonumber(d))
   480     end
   481     return fraction.invalid
   482   end
   483 end
   484 --//--
   486 function fraction:__tostring()
   487   if self.invalid then
   488     return "not_a_fraction"
   489   else
   490     return self.numerator .. "/" .. self.denominator
   491   end
   492 end
   494 function fraction.__eq(value1, value2)
   495   if value1.invalid or value2.invalid then
   496     return false
   497   else
   498     return
   499       value1.numerator == value2.numerator and
   500       value1.denominator == value2.denominator
   501   end
   502 end
   504 function fraction.__lt(value1, value2)
   505   if value1.invalid or value2.invalid then
   506     return false
   507   else
   508     return value1.float < value2.float
   509   end
   510 end
   512 function fraction.__le(value1, value2)
   513   if value1.invalid or value2.invalid then
   514     return false
   515   else
   516     return value1.float <= value2.float
   517   end
   518 end
   520 function fraction:__unm()
   521   return fraction(-self.numerator, self.denominator)
   522 end
   524 do
   526   local function extract(value1, value2)
   527     local n1, d1, n2, d2
   528     if getmetatable(value1) == fraction then
   529       n1 = value1.numerator
   530       d1 = value1.denominator
   531     elseif type(value1) == "number" then
   532       n1 = value1
   533       d1 = 1
   534     else
   535       error("Left operand of operator has wrong type.")
   536     end
   537     if getmetatable(value2) == fraction then
   538       n2 = value2.numerator
   539       d2 = value2.denominator
   540     elseif type(value2) == "number" then
   541       n2 = value2
   542       d2 = 1
   543     else
   544       error("Right operand of operator has wrong type.")
   545     end
   546     return n1, d1, n2, d2
   547   end
   549   function fraction.__add(value1, value2)
   550     local n1, d1, n2, d2 = extract(value1, value2)
   551     return fraction(n1 * d2 + n2 * d1, d1 * d2)
   552   end
   554   function fraction.__sub(value1, value2)
   555     local n1, d1, n2, d2 = extract(value1, value2)
   556     return fraction(n1 * d2 - n2 * d1, d1 * d2)
   557   end
   559   function fraction.__mul(value1, value2)
   560     local n1, d1, n2, d2 = extract(value1, value2)
   561     return fraction(n1 * n2, d1 * d2)
   562   end
   564   function fraction.__div(value1, value2)
   565     local n1, d1, n2, d2 = extract(value1, value2)
   566     return fraction(n1 * d2, d1 * n2)
   567   end
   569   function fraction.__pow(value1, value2)
   570     local n1, d1, n2, d2 = extract(value1, value2)
   571     local n1_abs = math.abs(n1)
   572     local d1_abs = math.abs(d1)
   573     local n2_abs = math.abs(n2)
   574     local d2_abs = math.abs(d2)
   575     local numerator, denominator
   576     if d2_abs == 1 then
   577       numerator = n1_abs
   578       denominator = d1_abs
   579     else
   580       numerator = 0
   581       while true do
   582         local t = numerator ^ d2_abs
   583         if t == n1_abs then break end
   584         if not (t < n1_abs) then return value1.float / value2.float end
   585         numerator = numerator + 1
   586       end
   587       denominator = 1
   588       while true do
   589         local t = denominator ^ d2_abs
   590         if t == d1_abs then break end
   591         if not (t < d1_abs) then return value1.float / value2.float end
   592         denominator = denominator + 1
   593       end
   594     end
   595     if n1 < 0 then
   596       if d2_abs % 2 == 1 then
   597         numerator = -numerator
   598       else
   599         return fraction.invalid
   600       end
   601     end
   602     if n2 < 0 then
   603       numerator, denominator = denominator, numerator
   604     end
   605     return fraction(numerator ^ n2_abs, denominator ^ n2_abs)
   606   end
   608 end
   612 ----------
   613 -- date --
   614 ----------
   616 date = create_new_type("date")
   618 do
   619   local c1 = 365             -- days of a non-leap year
   620   local c4 = 4 * c1 + 1      -- days of a full 4 year cycle
   621   local c100 = 25 * c4 - 1   -- days of a full 100 year cycle
   622   local c400 = 4 * c100 + 1  -- days of a full 400 year cycle
   623   local get_month_offset     -- function returning days elapsed within
   624                              -- the given year until the given month
   625                              -- (exclusive the given month)
   626   do
   627     local normal_month_offsets = {}
   628     local normal_month_lengths = {
   629       31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
   630     }
   631     local sum = 0
   632     for i = 1, 12 do
   633       normal_month_offsets[i] = sum
   634       sum = sum + normal_month_lengths[i]
   635     end
   636     function get_month_offset(year, month)
   637       if
   638         (((year % 4 == 0) and not (year % 100 == 0)) or (year % 400 == 0))
   639         and month > 2
   640       then
   641         return normal_month_offsets[month] + 1
   642       else
   643         return normal_month_offsets[month]
   644       end
   645     end
   646   end
   648   --[[--
   649   jd =                  -- days from January 1st 1970
   650   atom.date.ymd_to_jd(
   651     year,               -- year
   652     month,              -- month from 1 to 12
   653     day                 -- day from 1 to 31
   654   )
   656   This function calculates the days from January 1st 1970 for a given year, month and day.
   658   --]]--
   659   local offset = 0
   660   function date.ymd_to_jd(year, month, day)
   661     assert(is_integer(year), "Invalid year specified.")
   662     assert(is_integer(month), "Invalid month specified.")
   663     assert(is_integer(day), "Invalid day specified.")
   664     local calc_year = year - 1
   665     local n400 = math.floor(calc_year / 400)
   666     local r400 = calc_year % 400
   667     local n100 = math.floor(r400 / 100)
   668     local r100 = r400 % 100
   669     local n4 = math.floor(r100 / 4)
   670     local n1 = r100 % 4
   671     local jd = (
   672       c400 * n400 + c100 * n100 + c4 * n4 + c1 * n1 +
   673       get_month_offset(year, month) + (day - 1)
   674     )
   675     return jd - offset
   676   end
   677   offset = date.ymd_to_jd(1970, 1, 1)
   678   --//--
   680   --[[--
   681   year,                 -- year
   682   month,                -- month from 1 to 12
   683   day =                 -- day from 1 to 31
   684   atom.date.jd_to_ymd(
   685     jd,                 -- days from January 1st 1970
   686   )
   688   Given the days from January 1st 1970 this function returns year, month and day.
   690   --]]--
   691   function date.jd_to_ymd(jd)
   692     assert(is_integer(jd), "Invalid julian date specified.")
   693     local calc_jd = jd + offset
   694     assert(is_integer(calc_jd), "Julian date is out of range.")
   695     local n400 = math.floor(calc_jd / c400)
   696     local r400 = calc_jd % c400
   697     local n100 = math.floor(r400 / c100)
   698     local r100 = r400 % c100
   699     if n100 == 4 then n100, r100 = 3, c100 end
   700     local n4 = math.floor(r100 / c4)
   701     local r4 = r100 % c4
   702     local n1 = math.floor(r4 / c1)
   703     local r1 = r4 % c1
   704     if n1 == 4 then n1, r1 = 3, c1 end
   705     local year = 1 + 400 * n400 + 100 * n100 + 4 * n4 + n1
   706     local month = 1 + math.floor(r1 / 31)
   707     local month_offset = get_month_offset(year, month)
   708     if month < 12 then
   709       local next_month_offset = get_month_offset(year, month + 1)
   710       if r1 >= next_month_offset then
   711         month = month + 1
   712         month_offset = next_month_offset
   713       end
   714     end
   715     local day = 1 + r1 - month_offset
   716     return year, month, day
   717   end
   718   --//--
   719 end
   721 --[[--
   722 atom.date.invalid
   724 Value representing an invalid date.
   726 --]]--
   727 date.invalid = date:_create{
   728   jd = not_a_number,
   729   year = not_a_number, month = not_a_number, day = not_a_number,
   730   invalid = true
   731 }
   732 --//--
   734 --[[--
   735 d =                             -- date based on the given data
   736 atom.date:new{
   737   jd           = jd,            -- days since January 1st 1970
   738   year         = year,          -- year
   739   month        = month,         -- month from 1 to 12
   740   day          = day,           -- day from 1 to 31
   741   iso_weekyear = iso_weekyear,  -- year according to ISO 8601
   742   iso_week     = iso_week,      -- week number according to ISO 8601
   743   iso_weekday  = iso_weekday,   -- day of week from 1 for monday to 7 for sunday
   744   us_weekyear  = us_weekyear,   -- year
   745   us_week      = us_week,       -- week number according to US style counting
   746   us_weekday   = us_weekday     -- day of week from 1 for sunday to 7 for saturday
   747 }
   749 This method returns a new date value, based on given data.
   751 --]]--
   752 function date:new(args)
   753   local args = args
   754   if type(args) == "number" then args = { jd = args } end
   755   if type(args) == "table" then
   756     local year, month, day = args.year, args.month, args.day
   757     local jd = args.jd
   758     local iso_weekyear = args.iso_weekyear
   759     local iso_week     = args.iso_week
   760     local iso_weekday  = args.iso_weekday
   761     local us_week      = args.us_week
   762     local us_weekday   = args.us_weekday
   763     if
   764       type(year)  == "number" and
   765       type(month) == "number" and
   766       type(day)   == "number"
   767     then
   768       if
   769         is_integer(year)  and year >= 1  and year <= 9999 and
   770         is_integer(month) and month >= 1 and month <= 12  and
   771         is_integer(day)   and day >= 1   and day <= 31
   772       then
   773         local jd = date.ymd_to_jd(year, month, day)
   774         local year2, month2, day2 = date.jd_to_ymd(jd)
   775         if year == year2 and month == month2 and day == day2 then
   776           return date:_create{
   777             jd = date.ymd_to_jd(year, month, day),
   778             year = year2, month = month2, day = day2
   779           }
   780         else
   781           return date.invalid
   782         end
   783       else
   784         return date.invalid
   785       end
   786     elseif type(jd) == "number" then
   787       if is_integer(jd) and jd >= -719162 and jd <= 2932896 then
   788         local year, month, day = date.jd_to_ymd(jd)
   789         return date:_create{
   790           jd = jd, year = year, month = month, day = day
   791         }
   792       else
   793         return date.invalid
   794       end
   795     elseif
   796       type(year)        == "number" and not iso_weekyear and
   797       type(iso_week)    == "number" and
   798       type(iso_weekday) == "number"
   799     then
   800       if
   801         is_integer(year) and
   802         is_integer(iso_week)     and iso_week >= 0 and iso_week <= 53 and
   803         is_integer(iso_weekday)  and iso_weekday >= 1 and iso_weekday <= 7
   804       then
   805         local jan4 = date{ year = year, month = 1, day = 4 }
   806         local reference = jan4 - jan4.iso_weekday - 7  -- Sun. of week -1
   807         return date(reference + 7 * iso_week + iso_weekday)
   808       else
   809         return date.invalid
   810       end
   811     elseif
   812       type(iso_weekyear) == "number" and not year and
   813       type(iso_week)     == "number" and
   814       type(iso_weekday)  == "number"
   815     then
   816       if
   817         is_integer(iso_weekyear) and
   818         is_integer(iso_week)     and iso_week >= 0 and iso_week <= 53 and
   819         is_integer(iso_weekday)  and iso_weekday >= 1 and iso_weekday <= 7
   820       then
   821         local guessed = date{
   822           year        = iso_weekyear,
   823           iso_week    = iso_week,
   824           iso_weekday = iso_weekday
   825         }
   826         if guessed.invalid or guessed.iso_weekyear == iso_weekyear then
   827           return guessed
   828         else
   829           local year
   830           if iso_week <= 1 then
   831             year = iso_weekyear - 1
   832           elseif iso_week >= 52 then
   833             year = iso_weekyear + 1
   834           else
   835             error("Internal error in ISO week computation occured.")
   836           end
   837           return date{
   838             year = year, iso_week = iso_week, iso_weekday = iso_weekday
   839           }
   840         end
   841       else
   842         return date.invalid
   843       end
   844     elseif
   845       type(year) == "number" and
   846       type(us_week)     == "number" and
   847       type(us_weekday)  == "number"
   848     then
   849       if
   850         is_integer(year) and
   851         is_integer(us_week)     and us_week >= 0    and us_week <= 54   and
   852         is_integer(us_weekday)  and us_weekday >= 1 and us_weekday <= 7
   853       then
   854         local jan1 = date{ year = year, month = 1, day = 1 }
   855         local reference = jan1 - jan1.us_weekday - 7  -- Sat. of week -1
   856         return date(reference + 7 * us_week + us_weekday)
   857       else
   858         return date.invalid
   859       end
   860     end
   861   end
   862   error("Illegal arguments passed to date constructor.")
   863 end
   864 --//--
   866 --[[--
   867 atom.date:get_current()
   869 This function returns today's date.
   871 --]]--
   872 function date:get_current()
   873   local now = os.date("*t")
   874   return date{
   875     year = now.year, month = now.month, day = now.day
   876   }
   877 end
   878 --//--
   880 --[[--
   881 date =           -- date represented by the string
   882 atom.date:load(
   883   string         -- string representing a date
   884 )
   886 This method returns a date represented by the given string.
   888 --]]--
   889 function date:load(str)
   890   if str == nil or str == "" then
   891     return nil
   892   elseif type(str) ~= "string" then
   893     error("String expected")
   894   else
   895     local year, month, day = string.match(
   896       str, "^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"
   897     )
   898     if year then
   899       return date{
   900         year = tonumber(year),
   901         month = tonumber(month),
   902         day = tonumber(day)
   903       }
   904     else
   905       return date.invalid
   906     end
   907   end
   908 end
   909 --//--
   911 function date:__tostring()
   912   if self.invalid then
   913     return "invalid_date"
   914   else
   915     return string.format(
   916       "%04i-%02i-%02i", self.year, self.month, self.day
   917     )
   918   end
   919 end
   921 function date.getters:midnight()
   922   return timestamp{ year = self.year, month = self.month, day = self.day }
   923 end
   925 function date.getters:midday()
   926   return timestamp{
   927     year = self.year, month = self.month, day = self.day,
   928     hour = 12
   929   }
   930 end
   932 function date.getters:iso_weekday()  -- 1 = Monday
   933   return (self.jd + 3) % 7 + 1
   934 end
   936 function date.getters:us_weekday()  -- 1 = Sunday
   937   return (self.jd + 4) % 7 + 1
   938 end
   940 function date.getters:iso_weekyear()  -- ISO week-numbering year
   941   local year, month, day = self.year, self.month, self.day
   942   local iso_weekday      = self.iso_weekday
   943   if month == 1 then
   944     if
   945       (day == 3 and iso_weekday == 7) or
   946       (day == 2 and iso_weekday >= 6) or
   947       (day == 1 and iso_weekday >= 5)
   948     then
   949       return year - 1
   950     end
   951   elseif month == 12 then
   952     if
   953       (day == 29 and iso_weekday == 1) or
   954       (day == 30 and iso_weekday <= 2) or
   955       (day == 31 and iso_weekday <= 3)
   956     then
   957       return year + 1
   958     end
   959   end
   960   return year
   961 end
   963 function date.getters:iso_week()
   964   local jan4 = date{ year = self.iso_weekyear, month = 1, day = 4 }
   965   local reference = jan4.jd - jan4.iso_weekday - 6  -- monday of week 0
   966   return math.floor((self.jd - reference) / 7)
   967 end
   969 function date.getters:us_week()
   970   local jan1 = date{ year = self.year, month = 1, day = 1 }
   971   local reference = jan1.jd - jan1.us_weekday - 6  -- sunday of week 0
   972   return math.floor((self.jd - reference) / 7)
   973 end
   975 function date.__eq(value1, value2)
   976   if value1.invalid or value2.invalid then
   977     return false
   978   else
   979     return value1.jd == value2.jd
   980   end
   981 end
   983 function date.__lt(value1, value2)
   984   if value1.invalid or value2.invalid then
   985     return false
   986   else
   987     return value1.jd < value2.jd
   988   end
   989 end
   991 function date.__le(value1, value2)
   992   if value1.invalid or value2.invalid then
   993     return false
   994   else
   995     return value1.jd <= value2.jd
   996   end
   997 end
   999 function date.__add(value1, value2)
  1000   if getmetatable(value1) == date then
  1001     if getmetatable(value2) == date then
  1002       error("Can not add two dates.")
  1003     elseif type(value2) == "number" then
  1004       return date(value1.jd + value2)
  1005     else
  1006       error("Right operand of '+' operator has wrong type.")
  1007     end
  1008   elseif type(value1) == "number" then
  1009     if getmetatable(value2) == date then
  1010       return date(value1 + value2.jd)
  1011     else
  1012       error("Assertion failed")
  1013     end
  1014   else
  1015     error("Left operand of '+' operator has wrong type.")
  1016   end
  1017 end
  1019 function date.__sub(value1, value2)
  1020   if not getmetatable(value1) == date then
  1021     error("Left operand of '-' operator has wrong type.")
  1022   end
  1023   if getmetatable(value2) == date then
  1024     return value1.jd - value2.jd  -- TODO: transform to interval
  1025   elseif type(value2) == "number" then
  1026     return date(value1.jd - value2)
  1027   else
  1028     error("Right operand of '-' operator has wrong type.")
  1029   end
  1030 end
  1034 ---------------
  1035 -- timestamp --
  1036 ---------------
  1038 timestamp = create_new_type("timestamp")
  1040 --[[--
  1041 tsec =                          -- seconds since January 1st 1970 00:00
  1042 atom.timestamp.ymdhms_to_tsec(
  1043   year,                         -- year
  1044   month,                        -- month from 1 to 12
  1045   day,                          -- day from 1 to 31
  1046   hour,                         -- hour from 0 to 23
  1047   minute,                       -- minute from 0 to 59
  1048   second                        -- second from 0 to 59
  1049 )
  1051 Given the year, month, day, hour, minute and second, this function returns the number of seconds since January 1st 1970 00:00.
  1053 --]]--
  1054 function timestamp.ymdhms_to_tsec(year, month, day, hour, minute, second)
  1055   return
  1056     86400 * date.ymd_to_jd(year, month, day) +
  1057     3600 * hour + 60 * minute + second
  1058 end
  1059 --//--
  1061 --[[--
  1062 year,                      -- year
  1063 month,                     -- month from 1 to 12
  1064 day,                       -- day from 1 to 31
  1065 hour,                      -- hour from 0 to 23
  1066 minute,                    -- minute from 0 to 59
  1067 second =                   -- second from 0 to 59
  1068 atom.timestamp.tsec_to_ymdhms(
  1069   tsec                     -- seconds since January 1st 1970 00:00
  1070 )
  1072 Given the seconds since January 1st 1970 00:00, this function returns the year, month, day, hour, minute and second.
  1074 --]]--
  1075 function timestamp.tsec_to_ymdhms(tsec)
  1076   local jd   = math.floor(tsec / 86400)
  1077   local dsec = tsec % 86400
  1078   local year, month, day = date.jd_to_ymd(jd)
  1079   local hour   = math.floor(dsec / 3600)
  1080   local minute = math.floor((dsec % 3600) / 60)
  1081   local second = dsec % 60
  1082   return year, month, day, hour, minute, second
  1083 end
  1084 --//--
  1086 --[[--
  1087 atom.timestamp.invalid
  1089 Value representing an invalid timestamp.
  1091 --]]--
  1092 timestamp.invalid = timestamp:_create{
  1093   tsec = not_a_number,
  1094   year = not_a_number, month = not_a_number, day = not_a_number,
  1095   hour = not_a_number, minute = not_a_number, second = not_a_number,
  1096   invalid = true
  1097 }
  1098 --//--
  1100 --[[--
  1101 ts =                 -- timestamp based on given data
  1102 atom.timestamp:new{
  1103   tsec   = tsec,     -- seconds since January 1st 1970 00:00
  1104   year   = year,     -- year
  1105   month  = month,    -- month from 1 to 12
  1106   day    = day,      -- day from 1 to 31
  1107   hour   = hour,     -- hour from 0 to 23
  1108   minute = minute,   -- minute from 0 to 59
  1109   second = second    -- second from 0 to 59
  1110 }
  1112 This method returns a new timestamp value, based on given data.
  1114 --]]--
  1115 function timestamp:new(args)
  1116   local args = args
  1117   if type(args) == "number" then args = { tsec = args } end
  1118   if type(args) == "table" then
  1119     if not args.second then
  1120       args.second = 0
  1121       if not args.minute then
  1122         args.minute = 0
  1123         if not args.hour then
  1124           args.hour = 0
  1125         end
  1126       end
  1127     end
  1128     if
  1129       type(args.year)   == "number" and
  1130       type(args.month)  == "number" and
  1131       type(args.day)    == "number" and
  1132       type(args.hour)   == "number" and
  1133       type(args.minute) == "number" and
  1134       type(args.second) == "number"
  1135     then
  1136       if
  1137         is_integer(args.year) and
  1138         args.year >= 1 and args.year <= 9999 and
  1139         is_integer(args.month) and
  1140         args.month >= 1 and args.month <= 12 and
  1141         is_integer(args.day) and
  1142         args.day >= 1 and args.day <= 31 and
  1143         is_integer(args.hour) and
  1144         args.hour >= 0 and args.hour <= 23 and
  1145         is_integer(args.minute) and
  1146         args.minute >= 0 and args.minute <= 59 and
  1147         is_integer(args.second) and
  1148         args.second >= 0 and args.second <= 59
  1149       then
  1150         return timestamp:_create{
  1151           tsec = timestamp.ymdhms_to_tsec(
  1152             args.year, args.month, args.day,
  1153             args.hour, args.minute, args.second
  1154           ),
  1155           year   = args.year,
  1156           month  = args.month,
  1157           day    = args.day,
  1158           hour   = args.hour,
  1159           minute = args.minute,
  1160           second = args.second
  1161         }
  1162       else
  1163         return timestamp.invalid
  1164       end
  1165     elseif type(args.tsec) == "number" then
  1166       if
  1167         is_integer(args.tsec) and
  1168         args.tsec >= -62135596800 and args.tsec <= 253402300799
  1169       then
  1170         local year, month, day, hour, minute, second =
  1171           timestamp.tsec_to_ymdhms(args.tsec)
  1172         return timestamp:_create{
  1173           tsec = args.tsec,
  1174           year = year, month = month, day = day,
  1175           hour = hour, minute = minute, second = second
  1176         }
  1177       else
  1178         return timestamp.invalid
  1179       end
  1180     end
  1181   end
  1182   error("Invalid arguments passed to timestamp constructor.")
  1183 end
  1184 --//--
  1186 --[[--
  1187 ts =                          -- current date/time as timestamp
  1188 atom.timestamp:get_current()
  1190 This function returns the current date and time as timestamp.
  1192 --]]--
  1193 function timestamp:get_current()
  1194   local now = os.date("*t")
  1195   return timestamp{
  1196     year = now.year, month = now.month, day = now.day,
  1197     hour = now.hour, minute = now.min, second = now.sec
  1198   }
  1199 end
  1200 --//--
  1202 --[[--
  1203 ts =             -- timestamp represented by the string
  1204 atom.timestamp:load(
  1205   string         -- string representing a timestamp
  1206 )
  1208 This method returns a timestamp represented by the given string.
  1210 --]]--
  1211 function timestamp:load(str)
  1212   if str == nil or str == "" then
  1213     return nil
  1214   elseif type(str) ~= "string" then
  1215     error("String expected")
  1216   else
  1217     local year, month, day, hour, minute, second = string.match(
  1218       str,
  1219       "^([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])$"
  1220     )
  1221     if year then
  1222       return timestamp{
  1223         year   = tonumber(year),
  1224         month  = tonumber(month),
  1225         day    = tonumber(day),
  1226         hour   = tonumber(hour),
  1227         minute = tonumber(minute),
  1228         second = tonumber(second)
  1229       }
  1230     else
  1231       return timestamp.invalid
  1232     end
  1233   end
  1234 end
  1236 function timestamp:__tostring()
  1237   if self.invalid then
  1238     return "invalid_timestamp"
  1239   else
  1240     return string.format(
  1241       "%04i-%02i-%02i %02i:%02i:%02i",
  1242       self.year, self.month, self.day, self.hour, self.minute, self.second
  1243     )
  1244   end
  1245 end
  1247 function timestamp.getters:date()
  1248   return date{ year = self.year, month = self.month, day = self.day }
  1249 end
  1251 function timestamp.getters:time()
  1252   return time{
  1253     hour = self.hour,
  1254     minute = self.minute,
  1255     second = self.second
  1256   }
  1257 end
  1259 function timestamp.__eq(value1, value2)
  1260   if value1.invalid or value2.invalid then
  1261     return false
  1262   else
  1263     return value1.tsec == value2.tsec
  1264   end
  1265 end
  1267 function timestamp.__lt(value1, value2)
  1268   if value1.invalid or value2.invalid then
  1269     return false
  1270   else
  1271     return value1.tsec < value2.tsec
  1272   end
  1273 end
  1275 function timestamp.__le(value1, value2)
  1276   if value1.invalid or value2.invalid then
  1277     return false
  1278   else
  1279     return value1.tsec <= value2.tsec
  1280   end
  1281 end
  1283 function timestamp.__add(value1, value2)
  1284   if getmetatable(value1) == timestamp then
  1285     if getmetatable(value2) == timestamp then
  1286       error("Can not add two timestamps.")
  1287     elseif type(value2) == "number" then
  1288       return timestamp(value1.tsec + value2)
  1289     else
  1290       error("Right operand of '+' operator has wrong type.")
  1291     end
  1292   elseif type(value1) == "number" then
  1293     if getmetatable(value2) == timestamp then
  1294       return timestamp(value1 + value2.tsec)
  1295     else
  1296       error("Assertion failed")
  1297     end
  1298   else
  1299     error("Left operand of '+' operator has wrong type.")
  1300   end
  1301 end
  1303 function timestamp.__sub(value1, value2)
  1304   if not getmetatable(value1) == timestamp then
  1305     error("Left operand of '-' operator has wrong type.")
  1306   end
  1307   if getmetatable(value2) == timestamp then
  1308     return value1.tsec - value2.tsec  -- TODO: transform to interval
  1309   elseif type(value2) == "number" then
  1310     return timestamp(value1.tsec - value2)
  1311   else
  1312     error("Right operand of '-' operator has wrong type.")
  1313   end
  1314 end
  1318 ----------
  1319 -- time --
  1320 ----------
  1322 time = create_new_type("time")
  1324 function time.hms_to_dsec(hour, minute, second)
  1325   return 3600 * hour + 60 * minute + second
  1326 end
  1328 function time.dsec_to_hms(dsec)
  1329   local hour   = math.floor(dsec / 3600)
  1330   local minute = math.floor((dsec % 3600) / 60)
  1331   local second = dsec % 60
  1332   return hour, minute, second
  1333 end
  1335 --[[--
  1336 atom.time.invalid
  1338 Value representing an invalid time of day.
  1340 --]]--
  1341 time.invalid = time:_create{
  1342   dsec = not_a_number,
  1343   hour = not_a_number, minute = not_a_number, second = not_a_number,
  1344   invalid = true
  1345 }
  1346 --//--
  1348 --[[--
  1349 t =                 -- time based on given data
  1350 atom.time:new{
  1351   dsec = dsec,      -- seconds since 00:00:00
  1352   hour = hour,      -- hour from 0 to 23
  1353   minute = minute,  -- minute from 0 to 59
  1354   second = second   -- second from 0 to 59
  1355 }
  1357 This method returns a new time value, based on given data.
  1359 --]]--
  1360 function time:new(args)
  1361   local args = args
  1362   if type(args) == "number" then args = { dsec = args } end
  1363   if type(args) == "table" then
  1364     if not args.second then
  1365       args.second = 0
  1366       if not args.minute then
  1367         args.minute = 0
  1368       end
  1369     end
  1370     if
  1371       type(args.hour)   == "number" and
  1372       type(args.minute) == "number" and
  1373       type(args.second) == "number"
  1374     then
  1375       if
  1376         is_integer(args.hour) and
  1377         args.hour >= 0 and args.hour <= 23 and
  1378         is_integer(args.minute) and
  1379         args.minute >= 0 and args.minute <= 59 and
  1380         is_integer(args.second) and
  1381         args.second >= 0 and args.second <= 59
  1382       then
  1383         return time:_create{
  1384           dsec = time.hms_to_dsec(args.hour, args.minute, args.second),
  1385           hour   = args.hour,
  1386           minute = args.minute,
  1387           second = args.second
  1388         }
  1389       else
  1390         return time.invalid
  1391       end
  1392     elseif type(args.dsec) == "number" then
  1393       if
  1394         is_integer(args.dsec) and
  1395         args.dsec >= 0 and args.dsec <= 86399
  1396       then
  1397         local hour, minute, second =
  1398           time.dsec_to_hms(args.dsec)
  1399         return time:_create{
  1400           dsec = args.dsec,
  1401           hour = hour, minute = minute, second = second
  1402         }
  1403       else
  1404         return time.invalid
  1405       end
  1406     end
  1407   end
  1408   error("Invalid arguments passed to time constructor.")
  1409 end
  1410 --//--
  1412 --[[--
  1413 t =                      -- current time of day
  1414 atom.time:get_current()
  1416 This method returns the current time of the day.
  1418 --]]--
  1419 function time:get_current()
  1420   local now = os.date("*t")
  1421   return time{ hour = now.hour, minute = now.min, second = now.sec }
  1422 end
  1423 --//--
  1425 --[[--
  1426 t =              -- time represented by the string
  1427 atom.time:load(
  1428   string         -- string representing a time of day
  1429 )
  1431 This method returns a time represented by the given string.
  1433 --]]--
  1434 function time:load(str)
  1435   if str == nil or str == "" then
  1436     return nil
  1437   elseif type(str) ~= "string" then
  1438     error("String expected")
  1439   else
  1440     local hour, minute, second = string.match(
  1441       str,
  1442       "^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$"
  1443     )
  1444     if hour then
  1445       return time{
  1446         hour   = tonumber(hour),
  1447         minute = tonumber(minute),
  1448         second = tonumber(second)
  1449       }
  1450     else
  1451       return time.invalid
  1452     end
  1453   end
  1454 end
  1455 --//--
  1457 function time:__tostring()
  1458   if self.invalid then
  1459     return "invalid_time"
  1460   else
  1461     return string.format(
  1462       "%02i:%02i:%02i",
  1463       self.hour, self.minute, self.second
  1464     )
  1465   end
  1466 end
  1468 function time.__eq(value1, value2)
  1469   if value1.invalid or value2.invalid then
  1470     return false
  1471   else
  1472     return value1.dsec == value2.dsec
  1473   end
  1474 end
  1476 function time.__lt(value1, value2)
  1477   if value1.invalid or value2.invalid then
  1478     return false
  1479   else
  1480     return value1.dsec < value2.dsec
  1481   end
  1482 end
  1484 function time.__le(value1, value2)
  1485   if value1.invalid or value2.invalid then
  1486     return false
  1487   else
  1488     return value1.dsec <= value2.dsec
  1489   end
  1490 end
  1492 function time.__add(value1, value2)
  1493   if getmetatable(value1) == time then
  1494     if getmetatable(value2) == time then
  1495       error("Can not add two times.")
  1496     elseif type(value2) == "number" then
  1497       return time((value1.dsec + value2) % 86400)
  1498     else
  1499       error("Right operand of '+' operator has wrong type.")
  1500     end
  1501   elseif type(value1) == "number" then
  1502     if getmetatable(value2) == time then
  1503       return time((value1 + value2.dsec) % 86400)
  1504     else
  1505       error("Assertion failed")
  1506     end
  1507   else
  1508     error("Left operand of '+' operator has wrong type.")
  1509   end
  1510 end
  1512 function time.__sub(value1, value2)
  1513   if not getmetatable(value1) == time then
  1514     error("Left operand of '-' operator has wrong type.")
  1515   end
  1516   if getmetatable(value2) == time then
  1517     return value1.dsec - value2.dsec  -- TODO: transform to interval
  1518   elseif type(value2) == "number" then
  1519     return time((value1.dsec - value2) % 86400)
  1520   else
  1521     error("Right operand of '-' operator has wrong type.")
  1522   end
  1523 end
  1525 function time.__concat(value1, value2)
  1526   local mt1, mt2 = getmetatable(value1), getmetatable(value2)
  1527   if mt1 == date and mt2 == time then
  1528     return timestamp{
  1529       year = value1.year, month = value1.month, day = value1.day,
  1530       hour = value2.hour, minute = value2.minute, second = value2.second
  1531     }
  1532   elseif mt1 == time and mt2 == date then
  1533     return timestamp{
  1534       year = value2.year, month = value2.month, day = value2.day,
  1535       hour = value1.hour, minute = value1.minute, second = value1.second
  1536     }
  1537   elseif mt1 == time then
  1538     error("Right operand of '..' operator has wrong type.")
  1539   elseif mt2 == time then
  1540     error("Left operand of '..' operator has wrong type.")
  1541   else
  1542     error("Assertion failed")
  1543   end
  1544 end
  1548 return _M
