aboutsummaryrefslogtreecommitdiff
path: root/advtrains_line_automation/railwaytime.lua
diff options
context:
space:
mode:
authororwell <orwell@bleipb.de>2026-01-22 20:25:00 +0100
committerorwell <orwell@bleipb.de>2026-01-22 20:25:00 +0100
commit404690b08dca855ef57cde9fba0bd32caf4abef1 (patch)
tree3d0c29f2c12cfea0fe2e0293b9dd9ab565d82e4c /advtrains_line_automation/railwaytime.lua
parentd6469df4c73432ca953be9645a7db3626026f117 (diff)
RailwayTime: Extend to hours precision, wrap clock at 7 days, clip repeating times at day boundary
Diffstat (limited to 'advtrains_line_automation/railwaytime.lua')
-rw-r--r--advtrains_line_automation/railwaytime.lua382
1 files changed, 252 insertions, 130 deletions
diff --git a/advtrains_line_automation/railwaytime.lua b/advtrains_line_automation/railwaytime.lua
index 29ef110..ca14ba0 100644
--- a/advtrains_line_automation/railwaytime.lua
+++ b/advtrains_line_automation/railwaytime.lua
@@ -5,38 +5,66 @@
-- - Server stops/restarts
-- However, this means that implementing a "timetable" system using the "real time" is not practical. Therefore,
-- we introduce a custom time system, the RWT(Railway Time), which has nothing to do with RLT(Real-Life Time)
--- RWT has a time cycle of 1 hour. This should be sufficient for most train lines that will ever be built in Minetest.
--- A RWT looks like this: 37;25
--- The ; is to distinguish it from a normal RLT (which has colons e.g. 12:34:56). Left number is minutes, right number is seconds.
--- The minimum RWT is 00;00, the maximum is 59;59.
--- It is OK to leave one places out at either end, esp. when writing relative times, such as:
--- 43;3 22;0 2;30 0;10 ;10
--- Those places are then filled with zeroes. Indeed, ";" would be valid for 00;00 .
-
+-- RWT has a time cycle of 1 day. This should be sufficient for most train lines that will ever be built in Minetest.
+-- A RWT looks like this: 14;37;25
+-- The ; is to distinguish it from a normal RLT (which has colons e.g. 12:34:56). Left number is hours, middle number is minutes, right number is seconds.
+-- The minimum RWT is 00;00;00, the maximum is 23;59;59.
-- There is an "adapt mode", which was proposed by gpcf, and results in RWT automatically adapting itself to real-world time.
--- It works by shifting the minute/second after the realtime minute/second, adjusting the cycle value as needed.
+-- It works by shifting the hour/minute/second after the realtime hour/minute/second, adjusting the cycle value as needed.
+--[[
+There is an important distinction between:
--- Using negative times is discouraged. If you need a negative time, you may insert a minus (-) ONLY in the "c" place
+## Time (absolute time) ##
+Represents a time instant.
+It is identified by the 4 values cycle (day), hour, minute and second. These values are never negative.
---[[
-1;23;45 = {
- s=45,
- m=23,
- c=1, -- Cycle(~hour), not displayed most time
-}
+In string form, it is written as C;HH;MM;SS.
+The following short forms are allowed to display a RWT (but they do not uniquely identify a RWT):
+HH;MM;SS
+HH;MM
+It is discouraged to use the string form to store RWTs, the table form or seconds form is better.
+
+In table form:
+{ s=45, m=23, h=12, c=1 }
+
+In seconds form:
+c*24*60*60 + h*60*60 + m*60 + s
+
+Absolute time wraps around at a boundary of 7 cycles (valid cycle values are 0 to 6). If the difference between instants A and B
+is < 3.5 days, B is considered in the future, otherwise B is considered in the past.
+The RWT functions take this wrapping into account.
+
+## Interval (relative time) ##
+Represents a difference between times.
+
+It is commonly represented in seconds form as a number of seconds.
+It can be formatted as a string e.g.
+ 90 seconds = "+01;30"
+-90 seconds = "-01;30"
+When converting to a string, a + or - must be prepended.
+When parsing a string it is permissible to leave out the + if it is clear from the context that
+an interval is parsed.
+The meaning of string short forms differ from the time instant case:
+[+-]MM;SS (meaning when interval is parsed FROM a string, and the default when converting to string and HH==0 and C==0)
+[+-]HH;MM;SS (when HH!=0 and C==0)
+[+-]C;HH;MM;SS (when all !=0)
+[+-]HH;MM (permissible only when converting TO a string, to be used when it is displayed alongside an instant e.g. 05;30+00;04)
-Railway times can exist in 3 forms:
-- as table (see above)
-- as string (like "12;34")
-- as number (of seconds)
-Forms are automagically converted as needed by the converter functions to_*
-To be sure a rwt is in the required form, explicitly use a converter.
+]]--
-]]
local rwt = {}
+local CYCLE = 24*60*60
+local MODULUS = CYCLE*7
+local MODULUS_H = MODULUS / 2
+
+-- adjust so that it is in range [-MODULUS/2, MODULUS/2)
+local function i_adjust(sec)
+ return ((sec + MODULUS_H) % MODULUS) - MODULUS_H
+end
+
--Time Stamp (Seconds since start of world)
local e_time = 0
local e_has_loaded = false
@@ -62,7 +90,7 @@ local function adapt_real_time()
atlog("[lines][rwt] Skipping",secsfwd,"seconds forward to sync rwt (",rwt.to_string(rwttab),") to real time (",os.date("%H:%M:%S"),")")
- e_time = e_time + secsfwd
+ e_time = (e_time + secsfwd) % MODULUS
end
function rwt.set_time(t)
@@ -87,14 +115,14 @@ function rwt.step(dt)
if setting_rwt_real=="independent" then
-- Regular stepping with dtime
- e_time = e_time + dt
+ e_time = (e_time + dt) % MODULUS
else
-- advance with real-world time
local diff = os.time() - e_last_epoch
e_last_epoch = os.time()
if diff>0 then
- e_time = e_time + diff
+ e_time = (e_time + diff) % MODULUS
end
end
end
@@ -103,17 +131,35 @@ function rwt.now()
return rwt.to_table(e_time)
end
-function rwt.new(c, m, s)
+function rwt.new_t(c, h, m, s)
+ assert(c>=0 and c<7 and c==math.floor(c))
+ assert(h>=0 and h<24 and h==math.floor(h))
+ assert(m>=0 and m<60 and m==math.floor(m))
+ assert(s>=0 and s<60)
return {
c = c or 0,
+ h = h or 0,
m = m or 0,
s = s or 0
}
end
+
+function rwt.new_i(sign, c, h, m, s)
+ assert(math.abs(sign)==1)
+ assert(c>=0 and c<7 and c==math.floor(c))
+ assert(h>=0 and h<24 and h==math.floor(h))
+ assert(m>=0 and m<60 and m==math.floor(m))
+ assert(s>=0 and s<60)
+
+ return sign * (c*24*60*60 + h*60*60 + m*60 + s)
+end
+
+
function rwt.copy(rwtime)
local rwtimet = rwt.to_table(rwtime)
return {
c = rwtimet.c or 0,
+ h = rwtimet.h or 0,
m = rwtimet.m or 0,
s = rwtimet.s or 0
}
@@ -123,185 +169,261 @@ function rwt.to_table(rwtime)
if type(rwtime) == "table" then
return rwtime
elseif type(rwtime) == "string" then
- return rwt.parse(rwtime)
+ return rwt.parse_t(rwtime)
elseif type(rwtime) == "number" then
local res = {}
local seconds = atfloor(rwtime)
res.s = seconds % 60
local minutes = atfloor(seconds/60)
res.m = minutes % 60
- res.c = atfloor(minutes/60)
+ local hours = atfloor(minutes/60)
+ res.h = hours % 24
+ res.c = atfloor(hours/24)
return res
end
end
-function rwt.to_secs(rwtime, c_over)
+local function intv_to_table(rwintp)
+ if type(rwintp) == "number" then
+ local res = {}
+ local rwint = math.abs(rwintp)
+ if rwint~=0 then
+ res.sign = rwintp/rwint -- 1 or -1
+ else
+ res.sign = 1
+ end
+ local seconds = atfloor(rwint)
+ res.s = seconds % 60
+ local minutes = atfloor(seconds/60)
+ res.m = minutes % 60
+ local hours = atfloor(minutes/60)
+ res.h = hours % 24
+ res.c = atfloor(hours/24)
+ return res
+ else
+ error("intv_to_table needs number")
+ end
+end
+
+-- time instant to seconds
+function rwt.t_sec(rwtime)
local res = rwtime
if type(rwtime) == "string" then
- res = rwt.parse(rwtime)
+ res = rwt.parse_t(rwtime)
elseif type(rwtime) == "number" then
return rwtime
end
if type(res)=="table" then
- return (c_over or res.c)*60*60 + res.m*60 + res.s
+ return res.c*60*60*24 + res.h*60*60 + res.m*60 + res.s
+ end
+end
+
+-- interval to seconds
+function rwt.i_sec(rwint)
+ if type(rwint) == "string" then
+ return rwt.parse_i(rwint)
+ elseif type(rwint) == "number" then
+ return rwint
+ elseif type(rwint)=="table" then
+ return res.sign * (res.c*60*60*24 + res.h*60*60 + res.m*60 + res.s)
end
end
-function rwt.to_string(rwtime_p, no_cycle)
+-- time to string
+-- fmt:
+-- 0, nil or false: full format C;HH;MM;SS
+-- 1 or true: format HH;MM;SS
+-- 2: format MM;SS
+-- 3: format HH;MM
+function rwt.t_str(rwtime_p, fmt)
local rwtime = rwt.to_table(rwtime_p)
- if rwtime.c~=0 and not no_cycle then
- return string.format("%d;%02d;%02d", rwtime.c, rwtime.m, rwtime.s)
- else
+ if fmt==0 or not fmt then
+ return string.format("%d;%02d;%02d;%02d", rwtime.c, rwtime.h, rwtime.m, rwtime.s)
+ elseif fmt==1 or fmt==true then
+ return string.format("%02d;%02d;%02d", rwtime.h, rwtime.m, rwtime.s)
+ elseif fmt==2 then
return string.format("%02d;%02d", rwtime.m, rwtime.s)
+ elseif fmt==3 then
+ return string.format("%02d;%02d", rwtime.h, rwtime.m)
+ end
+end
+-- compatibility alias
+rwt.to_string = rwt.t_str
+
+-- interval to string
+function rwt.i_str(rwint_p, omit_plus, omit_second)
+ local rwint = intv_to_table(rwint_p)
+ -- sign
+ local sic = rwint.sign==1 and (omit_plus and "" or "+") or "-"
+ -- find out formatting
+ if omit_second then
+ if rwint.c~=0 then
+ return string.format("%s%d;%02d;%02d", sic, rwint.c, rwint.h, rwint.m)
+ else
+ return string.format("%s%02d;%02d", sic, rwint.h, rwint.m)
+ end
+ else
+ if rwint.c~=0 then
+ return string.format("%s%d;%02d;%02d;%02d", sic, rwint.c, rwint.h, rwint.m, rwint.s)
+ elseif rwint.h~=0 then
+ return string.format("%s%02d;%02d;%02d", sic, rwint.h, rwint.m, rwint.s)
+ else
+ return string.format("%s%02d;%02d", sic, rwint.m, rwint.s)
+ end
end
end
---
-local function v_n(str, cpl)
- if not str then return nil end
+local function v_n(str)
if str == "" then
return 0
end
- local n = tonumber(str)
- if not cpl and (n<0 or n>59) then
- return nil
- end
- return n
+ return tonumber(str)
end
+-- parse an interval (the default case - parsing an instant is rare)
function rwt.parse(str)
--atdebug("parse",str)
+ --4-value form
+ local sic, str_c, str_h, str_m, str_s = string.match(str, "^([%-%+]?)(%d?%d?);(%d%d);(%d%d);(%d?%d?)$")
+ if sic and str_c and str_h and str_m and str_s then
+ --atdebug("4v", sic, str_c, str_h, str_m, str_s)
+ local c, h, m, s = v_n(str_c), v_n(str_h), v_n(str_m), v_n(str_s)
+ if c and h and m and s then
+ return rwt.new_i(sic=="-" and -1 or 1, c,h,m,s)
+ end
+ end
--3-value form
- local str_c, str_m, str_s = string.match(str, "^(%-?%d?%d?);(%d%d);(%d?%d?)$")
- if str_c and str_m and str_s then
- --atdebug("3v",str_c, str_m, str_s)
- local c, m, s = v_n(str_c, true), v_n(str_m), v_n(str_s)
- if c and m and s then
- return rwt.new(c,m,s)
+ local sic, str_h, str_m, str_s = string.match(str, "^([%-%+]?)(%d?%d?);(%d%d);(%d?%d?)$")
+ if sic and str_h and str_m and str_s then
+ --atdebug("3v", sic, str_h, str_m, str_s)
+ local c, h, m, s = 0, v_n(str_h), v_n(str_m), v_n(str_s)
+ if c and h and m and s then
+ return rwt.new_i(sic=="-" and -1 or 1, c,h,m,s)
end
end
--2-value form
- local str_m, str_s = string.match(str, "^(%d?%d?);(%d?%d?)$")
- if str_m and str_s then
- --atdebug("2v",str_m, str_s)
- local m, s = v_n(str_m), v_n(str_s)
- if m and s then
- return rwt.new(0,m,s)
+ local sic, str_m, str_s = string.match(str, "^([%-%+]?)(%d?%d?);(%d?%d?)$")
+ if sic and str_m and str_s then
+ --atdebug("2v", sic, str_m, str_s)
+ local c, h, m, s = 0, 0, v_n(str_m), v_n(str_s)
+ if c and h and m and s then
+ return rwt.new_i(sic=="-" and -1 or 1, c,h,m,s)
end
end
end
+rwt.parse_i = rwt.parse
----
-
-function rwt.add(t1, t2)
- local t1s = rwt.to_secs(t1)
- local t2s = rwt.to_secs(t2)
- return rwt.to_table(t1s + t2s)
+-- parse a time instant - only the 4-value form is allowed!
+function rwt.parse_t(str)
+ --atdebug("parse",str)
+ --4-value form
+ local str_c, str_h, str_m, str_s = string.match(str, "^(%d?%d?);(%d%d);(%d%d);(%d?%d?)$")
+ if str_c and str_h and str_m and str_s then
+ --atdebug("4v", str_c, str_h, str_m, str_s)
+ local c, h, m, s = v_n(str_c), v_n(str_h), v_n(str_m), v_n(str_s)
+ if c and h and m and s then
+ return rwt.new_t(c,h,m,s)
+ end
+ end
end
--- How many seconds FROM t1 TO t2
-function rwt.diff(t1, t2)
- local t1s = rwt.to_secs(t1)
- local t2s = rwt.to_secs(t2)
- return t2s - t1s
-end
+---
--- Subtract t2 from t1 (inverted argument order compared to diff())
-function rwt.sub(t1, t2)
- return rwt.to_table(rwt.diff(t2, t1))
+-- add a time and an interval. The result is a time (table form)
+function rwt.add(t1, i)
+ local t1s = rwt.t_sec(t1)
+ local t2s = rwt.i_sec(i)
+ return rwt.to_table( (t1s + t2s) % MODULUS )
end
--- Adjusts t2 by thresh and then returns time from t1 to t2
-function rwt.adj_diff(t1, t2, thresh)
- local newc = rwt.adjust_cycle(t2, thresh, t1)
- local t1s = rwt.to_secs(t1)
- local t2s = rwt.to_secs(t2, newc)
- return t1s - t2s
+-- subtract a time and an interval. The result is a time (table form)
+function rwt.sub(t1, i)
+ local t1s = rwt.t_sec(t1)
+ local t2s = rwt.i_sec(i)
+ return rwt.to_table( (t1s - t2s) % MODULUS )
end
-
--- Threshold values
--- "reftime" is the time to which this is made relative and defaults to now.
-rwt.CA_FUTURE = 60*60 - 1 -- Selected so that time lies at or in the future of reftime (at nearest point in time)
-rwt.CA_FUTURES = 60*60 -- Same, except when times are equal, advances one full cycle
-rwt.CA_PAST = 0 -- Selected so that time lies at or in the past of reftime
-rwt.CA_PASTS = -1 -- Same, except when times are equal, goes back one full cycle
-rwt.CA_CENTER = 30*60 -- If time is within past 30 minutes of reftime, selected as past, else selected as future.
-
--- Adjusts the "cycle" value of a railway time to be in some relation to reftime.
--- Returns new cycle
-function rwt.adjust_cycle(rwtime, reftime_p, thresh)
- local reftime = reftime_p or rwt.now()
-
- local reftimes = rwt.to_secs(reftime)
-
- local rwtimes = rwt.to_secs(rwtime, 0)
- local timeres = reftimes + thresh - rwtimes
- local cycles = atfloor(timeres / (60*60))
-
- return cycles
+-- How many seconds FROM t1 TO t2 (two time instants, result is an interval)
+function rwt.diff(t1, t2)
+ local t1s = rwt.t_sec(t1)
+ local t2s = rwt.t_sec(t2)
+ return i_adjust(t2s - t1s)
end
-function rwt.adjust(rwtime, reftime, thresh)
- local cp = rwt.copy(rwtime)
- cp.c = rwt.adjust_cycle(rwtime, reftime, thresh)
- return cp
+-- Whether t1 is (strictly) after t2
+function rwt.is_after(t1, t2)
+ return rwt.diff(t2,t1) > 0
end
--- Useful for departure times: returns time (in seconds)
--- until the next (adjusted FUTURE) occurence of deptime is reached
--- in this case, rwtime is used as reftime and deptime should lie in the future of rwtime
--- rwtime defaults to NOW
-function rwt.get_time_until(deptime, rwtime_p)
- local rwtime = rwtime_p or rwt.now()
- return rwt.adj_diff(rwtime, deptime, rwt.CA_FUTURE)
+-- Whether t1 is (strictly) before t2
+function rwt.is_before(t1, t2)
+ return rwt.diff(t1,t2) > 0
end
-
-- Helper functions for handling "repeating times" (rpt)
-- Those are generic declarations for time intervals like "every 5 minutes", with an optional offset
--- ( /02;00-00;45 in timetable syntax
+-- Note the following:
+-- - rwtime is a time instant, rpt_interval and rpt_offset are intervals
+-- - repeating times are hard-capped at cycle (24-hour) boundary
+-- - if rpt_interval is given as nil, there is exactly 1 match per cycle and it is at X;00;00;00 + offset ( equals a time-of-day)
+-- - the offset determines where the "reference cycle" starts e.g. if the off=05;00;00 and int=03;45;00, starting from 0;05;00 the times would be
+-- 05;00, 08;45, ..., 20;00, 23;45, 1;03;30, then next cycle starts 1;05;00
+-- - if the cycle is not a multiple of the interval like in the previous example, the very last instance is not considered when it is closer than interval/2 to the cycle boundary
+-- in the example above the distance between 1;03;30 and 1;05;00 is only 90s, which is smaller than interval/2
+-- It is preferred to therefore have a longer interval
+-- and the actual sequence will be ..., 20;00, 23;45, 1;05;00
-- Get the time (in seconds) until the next time this rpt occurs
function rwt.time_to_next_rpt(rwtime, rpt_interval, rpt_offset)
- local rpti_s = rwt.to_secs(rpt_interval)
+ if not rpt_interval then rpt_interval = CYCLE end
+
+ local rwtime_s = rwt.t_sec(rwtime)
+ local rpti_s = rwt.i_sec(rpt_interval)
+ local rpto_s = rwt.i_sec(rpt_offset)
+ local otimcyc = (rwtime_s - rpto_s) % CYCLE -- time into current cycle, offset by the rpt offset
+ -- otimcyc is the time into a cycle which is assumed to start at rpto_s
+ local lasttime = otimcyc - (otimcyc % rpti_s)
+ local nexttime = lasttime + rpti_s -- next occurence is last occurence plus interval
+
+ if nexttime > (CYCLE - rpti_s/2) then -- last time cutoff rule, and overflow of cycle
+ nexttime = CYCLE
+ end
- return (rpti_s - rwt.time_from_last_rpt(rwtime, rpti_s, rpt_offset)) % rpti_s
- -- Modulo is just there to clip a false value of rpti_s to 0
+ return nexttime - otimcyc
end
-- Get the time (in seconds) since the last time this rpt occured
function rwt.time_from_last_rpt(rwtime, rpt_interval, rpt_offset)
- local rwtime_s = rwt.to_secs(rwtime)
- local rpti_s = rwt.to_secs(rpt_interval)
- local rpto_s = rwt.to_secs(rpt_offset)
+ if not rpt_interval then rpt_interval = CYCLE end
- return ((rwtime_s - rpto_s) % rpti_s)
+ local rwtime_s = rwt.t_sec(rwtime)
+ local rpti_s = rwt.i_sec(rpt_interval)
+ local rpto_s = rwt.i_sec(rpt_offset)
+ local otimcyc = (rwtime_s - rpto_s) % CYCLE -- time into current cycle, offset by the rpt offset
+ -- otimcyc is the time into a cycle which is assumed to start at rpto_s
+ local lasttime = otimcyc - (otimcyc % rpti_s)
+
+ if lasttime > (CYCLE - rpti_s/2) then -- last time cutoff rule
+ lasttime = lasttime - rpti_s
+ end
+
+ return otimcyc - lasttime
end
-- From rwtime, get the next time that is divisible by rpt_interval offset by rpt_offset
function rwt.next_rpt(rwtime, rpt_interval, rpt_offset)
- local rwtime_s = rwt.to_secs(rwtime)
- local rpti_s = rwt.to_secs(rpt_interval)
- local time_from_last = rwt.time_from_last_rpt(rwtime_s, rpti_s, rpt_offset)
-
- local res_s = rwtime_s - time_from_last + rpti_s
-
- return rwt.to_table(res_s)
+ local time_to_next = rwt.time_to_next_rpt(rwtime, rpt_interval, rpt_offset)
+ return rwt.add(rwtime, time_to_next)
end
-- from rwtime, get the last time that this rpt matched (which is actually just next_rpt - rpt_offset
function rwt.last_rpt(rwtime, rpt_interval, rpt_offset)
- local rwtime_s = rwt.to_sec(rwtime)
- local rpti_s = rwt.to_sec(rpt_interval)
local time_from_last = rwt.time_from_last_rpt(rwtime, rpt_interval, rpt_offset)
-
- local res_s = rwtime_s - time_from_last
-
- return rwt.to_table(res_s)
+ return rwt.sub(rwtime, time_from_last)
end
function rwt.to_os_time(rwtime)