# When - display a when calendar # Copyright (C) 2023 Duane Robertson # This program is only based on the data format used by # when, and does not include any of the original code. # https://www.lightandmatter.com/when/when.html # when.gd # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . extends Node2D const DAY = 24 * 60 * 60 const FILE_NAME = 'res://calendar' const FUTURE = 365.0 # 365.0 * 10.0 var output:RichTextLabel var reg_all_digit:RegEx var reg_birth_year:RegEx var reg_exp:RegEx var reg_simple_date:RegEx var months := ['jan', 'feb', 'mar', 'apr', 'may', 'jun', \ 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] var weekdays := ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] var operator := { '%' : 7, '-' : 6, '<' : 5, '>' : 5, '<=' : 5, '>=' : 5, '=' : 4, '!=' : 4, '!' : 3, '&' : 2, '|' : 1, } # Called when the node enters the scene tree for the first time. func _ready(): var sdr := '(\\d{4}\\*?|\\*) ([a-zA-Z]{3}|\\*) (\\d{1,2}|\\*)' reg_simple_date = RegEx.new() reg_simple_date.compile(sdr) reg_birth_year = RegEx.new() reg_birth_year.compile('(\\d{4})\\*') reg_exp = RegEx.new() var ops = '%|\\-|<|>|<=|>=|=|!=|!|&|\\|' reg_exp.compile('\\s*([a-z0-9]+|' + ops + ')\\s*') reg_all_digit = RegEx.new() reg_all_digit.compile('^[0-9]+$') assert(reg_simple_date.search('2022 apr 02')) output = RichTextLabel.new() # output.bbcode_enabled = true output.size = Vector2(640, 1280) output.position = Vector2(40, 0) output.set('theme_override_fonts/normal_font', preload('res://art/Roboto-Regular.ttf')) output.set('theme_override_font_sizes/normal_font_size', 40) # output.add_theme_font_override('Normal', preload('res://art/Roboto-Bold.ttf')) # print(output.get_theme_font('default')) # print(output.get_theme_font_size('default')) add_child(output) # output.add_theme_font_size_override('default', 150) # print(output.get_theme_font_size('default')) var cal:String = FileAccess.get_file_as_string(FILE_NAME) var epoch = Time.get_unix_time_from_system() var outd:Array = calendar(cal, epoch) print_calendar(outd, epoch) # print(output.text) # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta): pass func bday(num:int) -> String: var lnum = num % 10 var snum = int(num / 10) % 10 if snum == 1: return '%dth' % num elif lnum == 1: return '%dst' % num elif lnum == 2: return '%dnd' % num elif lnum == 3: return '%drd' % num else: return '%dth' % num func cal_match_dmy(md:Dictionary, date:Dictionary): if not ('*' in md.year or int(md.year) == date.year): return if not ('*' in md.month or md.month.to_lower() == months[date.month-1]): return if not ('*' in md.day or int(md.day) == date.day): return if '*' in md.year and len(md.year) > 1: var bym = reg_birth_year.search(md.year) if bym: md.birth_year = bym.get_string(1) return md func cal_match_rpn(mt:Dictionary, date): # Evaluate the rpn expression. var stack := [] var q = mt.rpn.duplicate() while not q.is_empty(): var t = q.pop_front() if operator.has(t): var a var o1 var o2 if t == '!': # only unary operator o1 = stack.pop_front() else: o2 = stack.pop_front() o1 = stack.pop_front() if t == '!': a = not o1 elif t == '%': a = int(o1) % int(o2) elif t == '-': a = int(o1) - int(o2) elif t == '=': a = (o1 == o2) elif t == '!=': a = (o1 != o2) elif t == '&': a = (o1 and o2) elif t == '|': a = (o1 or o2) elif t == '<': a = (o1 < o2) elif t == '>': a = (o1 > o2) elif t == '<=': a = (o1 <= o2) elif t == '>=': a = (o1 >= o2) else: assert(false) if a != null: stack.push_front(a) else: stack.push_front(replace_variables(t, date)) assert(len(stack) == 1) if stack.pop_front(): return {} func calendar(data_in:String, epoch) -> Array: var out := '' var outd := [] var data := data_convert(data_in) for ep in range(epoch - DAY * 2, epoch + DAY * int(FUTURE), DAY): var date = Time.get_datetime_dict_from_unix_time(ep) get_julian(date) for dt in data: var show if dt.type == 'simple': show = cal_match_dmy(dt, date) elif dt.type == 'exp': show = cal_match_rpn(dt, date) if show != null: if show.has('birth_year') and '\\a' in dt.note: var bdays = bday(date.get('year') - int(show.get('birth_year'))) var n = dt.note.replace('\\a', '%s' % bdays) outd.append([date, n]) # if 'birth_year' in show and r'\a' in dt.get('note'): # bdays = bday(date.get('year') - int(show.get('birth_year'))) # n = dt.get('note').replace(r'\a', '%s' % bdays) # outd.append([date, n]) else: outd.append([date, dt.note]) return outd func ctag(s:String, c:Color) -> String: # return '[color=#' + c.to_html().right(2) + ']' + s + '[/color]' return '[color=#' + c.to_html() + ']' + s + '[/color]' func data_convert(data_in:String) -> Array: var data:Array var data1:Array = data_in.split('\n') for i in len(data1): var s = data1[i].strip_edges() if s.substr(0, 1) == '#': continue var d = s.split(',') if len(d) != 2: continue for j in len(d): d[j] = d[j].strip_edges() var m = reg_simple_date.search(d[0]) if m: var md := { type = 'simple', year = m.get_string(1), month = m.get_string(2), day = m.get_string(3), note = d[1], } data.append(md) else: var md := { type = 'exp', rpn = make_rpn(d[0]), note = d[1], } data.append(md) return data func dlog(s:String): output.append_text(s + '\n') func get_days_in_month(date) -> int: var ds:int = 31 if date.month in [4,6,9,11]: ds = 30 elif date.month == 2: ds = 28 if date.year % 4 == 0: ds = 29 return ds func get_julian(date:Dictionary): var p1:int = (date.month - 14) / 12 var p2:int = date.year + 4800 + p1 var julian = (1461 * p2) / 4 \ + (367 * (date.month - 2 - 12 * p1)) / 12 \ - (3 * ((p2 + 100) / 100)) / 4 \ + date.day - 32075 - 2400000 ############################################## # When uses modified julian, so this is wrong. ############################################## var jn:float = julian + (date.hour - 12) / 24.0 \ + date.minute / 1440.0 + date.second / 86400.0 ############################################## date.jn = jn date.julian = julian func get_week(date:Dictionary, reverse:=false) -> int: var a:int = int(ceil(date.day / 7.0)) if reverse: var ds := get_days_in_month(date) a = int(ceil((ds - date.day + 1) / 7.0)) return a func make_rpn(s:String) -> Array: # Change the infix expression to reverse # polish using the shunting yard algorithm. var toks_in = reg_exp.search_all(s) var toks = [ ] for tok in toks_in: toks.append(tok.get_string(1)) assert(toks[-1] is String) var outq := [] var opq := [] while not toks.is_empty(): var t:String = toks.pop_front() if operator.has(t): while not opq.is_empty() and opq[-1] != '(' \ and operator[opq[-1]] >= operator[t]: outq.push_back(opq.pop_back()) opq.push_back(t) elif t == '(': opq.push_back(t) elif t == ')': while not opq.is_empty() and opq[-1] != '(': assert(not opq.is_empty()) outq.push_back(opq.pop_back()) assert(opq[-1] == '(') opq.pop_back() else: if reg_all_digit.search(t): outq.push_back(int(t)) elif t is String and t.to_lower() in months: outq.push_back(months.find(t.to_lower())+1) elif t is String and t.to_lower() in weekdays: outq.push_back(weekdays.find(t.to_lower())) else: outq.push_back(t) while not opq.is_empty(): assert(opq[-1] != '(') outq.push_back(opq.pop_back()) return outq func print_calendar(outd:Array, epoch:int): var date = Time.get_datetime_dict_from_system() get_julian(date) var date_out = [ scase(weekdays[date.weekday]), int(date.year), scase(months[date.month-1]), int(date.day) ] dlog('It is %s, %4d %s %2d\n' % date_out) var lastd var displayed = {} var c:Color = Color(0.9, 0.9, 0.9) for it in outd: if displayed.has(it[1]): continue var dist:int = abs(date.julian - it[0].julian) var d:float = float(0.5 + (FUTURE - dist) / FUTURE * 0.5) if lastd != it[0].julian: if lastd != null: dlog('') date_out = [ int(it[0].year), scase(months[it[0].month-1]), int(it[0].day) ] lastd = it[0].julian if dist < 2: if it[0].julian < date.julian: d = d - 0.3 c = Color(d, d, d) date_out.push_front('Yesterday') elif it[0].julian > date.julian: c = Color(d * 0.6, d, d * 0.6) date_out.push_front('Tomorrow') else: c = Color(d * 0.6, d, d * 0.6) date_out.push_front('Today') dlog(ctag('%s, %4d %s %2d', c) % date_out) else: if it[0].julian < date.julian: d = d - 0.3 c = Color(d, d, d) date_out.push_front(scase(weekdays[it[0].weekday])) dlog(ctag('%s, %4d %s %2d', c) % date_out) if it[0].julian >= date.julian: displayed[it[1]] = true dlog((ctag('\t%s', c)) % [it[1]]) #func print_rpn(q:Array) -> void: # var s = '' # for t in q: # s += ',' + t # dlog(s) func replace_variables(i_t, date:Dictionary): var t = i_t if not t is String: return t if t == 'j': t = date.julian elif t == 'm': t = date.month elif t == 'd': t = date.day elif t == 'w': t = date.weekday elif t == 'y': t = date.year elif t == 'a': t = get_week(date, false) elif t == 'b': t = get_week(date, true) elif t == 'e': ######################## # Add Easter... someday. ######################## t = 999999 ######################## return t func scase(s:String) -> String: return s[0].to_upper() + s.substr(1)