diff options
| author | orwell <orwell@bleipb.de> | 2026-01-22 20:25:00 +0100 |
|---|---|---|
| committer | orwell <orwell@bleipb.de> | 2026-01-22 20:25:00 +0100 |
| commit | 404690b08dca855ef57cde9fba0bd32caf4abef1 (patch) | |
| tree | 3d0c29f2c12cfea0fe2e0293b9dd9ab565d82e4c /advtrains_line_automation/railwaytime.lua | |
| parent | d6469df4c73432ca953be9645a7db3626026f117 (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.lua | 382 |
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) |
