package main import math "core:math" import "core:fmt" import "core:os" import "core:strings" import "core:strconv" import "core:slice" import "core:sort" // // --- STRUCTURES --- // Weekday :: enum{ Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday, } Delta :: struct { minutes : int, hours : int, days : int, } Moment :: struct { minutes : int, hours : int, day : int, month : int, year : int, } Timeblock :: struct { start : Moment, end : Moment, value : f32, reason : string, title : string, } Fractionpair :: struct { start : f32, end : f32, } Workday :: struct { call : Moment, wrap : Moment, planned_wrap : Moment, // blocks is over 12, // because lunch breaks // cause more blocks blocks : [16]Timeblock, // Fractions store how long // since the workday's // preceding midnight // a timesplit occurs. // They're pairs so they // can exactly map to each // timeblock's start and end fractions : [16]Fractionpair, total_timeblocks : int, price: f32, } // // --- MAJOR PROCEDURES --- // windIndividual :: proc(input_moment: ^Moment, minutes: int, hours: int, days: int) { // Adding minutes input_moment.minutes += minutes for input_moment.minutes > 59 { input_moment.minutes -= 60 input_moment.hours += 1 } for input_moment.minutes < 0 { input_moment.minutes += 60 input_moment.hours -= 1 } // Adding hours input_moment.hours += hours for input_moment.hours > 23 { input_moment.hours -= 24 input_moment.day += 1 } for input_moment.hours < 0 { input_moment.hours += 24 input_moment.day -= 1 } // Adding days input_moment.day += days current_month_length: int = days_in(input_moment.month, input_moment.year) for input_moment.day > current_month_length { input_moment.day -= current_month_length input_moment.month += 1 if input_moment.month > 12 { input_moment.month -= 12 input_moment.year += 1 } current_month_length = days_in(input_moment.month, input_moment.year) } for input_moment.day < 1 { input_moment.month -= 1 if input_moment.month < 1 { input_moment.month += 12 input_moment.year -= 1 } current_month_length = days_in(input_moment.month, input_moment.year) input_moment.day += current_month_length } return } windByDelta :: proc(moment: ^Moment, delta: Delta) { wind(moment, delta.minutes, delta.hours, delta.days) return } wind :: proc{windIndividual, windByDelta} timesplit :: proc(block: Timeblock, splitpoint: Moment) -> (first_half: Timeblock, second_half: Timeblock) { // Splits a timeblock at splitpoint. if sortable(splitpoint) < sortable(block.start) || sortable(splitpoint) > sortable(block.end) || splitpoint == block.start || splitpoint == block.end { fmt.println("WHOOPS: Splitpoint is outside timeblock range!") fmt.println("Timeblock:", toString(block)) fmt.println("Splitpoint:", toString(splitpoint)) second_half = block return } first_half = {block.start, splitpoint, block.value, block.reason, ""} second_half = {splitpoint, block.end, block.value, block.reason, ""} return } upvalue :: proc(input_block: ^Timeblock, value: f32, reason: string) { block: ^Timeblock = input_block if value > block.value { block.value = value block.reason = reason } } importICS :: proc(path: string, verbose: bool = false) -> ([dynamic]Timeblock, bool) { output: [dynamic]Timeblock c : Timeblock raw, err := os.read_entire_file_from_path(path, allocator=context.temp_allocator) content := string(raw) i := 1 line_nr := 1 if err != nil { // TODO: Actually check the content to see if it is an ICS file. fmt.eprintf("ERROR: No file found at: \"%v\"", path) return output, false } for line in strings.split_lines_iterator(&content) { // BUG: This assumes that there will never be a line shorter than 10. // That means this will try reading out of bounds at some point. if len(line)>=10 { if line[0:10]=="DTSTART;TZ" { // grab the timestamp from the end of the line, and set start to it ll := len(line) if verbose do fmt.println("Found a DTSTART!") if verbose do fmt.println("length of line:", ll) if verbose do fmt.println(line) date_start : int if verbose do fmt.printf("Time: %s:%s\n", line[ll-6:ll-4], line[ll-4:ll-2]) if verbose do fmt.printf("Hours: %s\n", line[ll-6:ll-4]) c.start.hours, _ = strconv.parse_int(line[ll-6:ll-4]) if verbose do fmt.printf("Minutes: %s\n", line[ll-4:ll-2]) c.start.minutes, _ = strconv.parse_int(line[ll-4:ll-2]) if verbose do fmt.printf("Day: %s\n", line[ll-9:ll-7]) c.start.day, _ = strconv.parse_int(line[ll-9:ll-7]) if verbose do fmt.printf("Month: %s\n", line[ll-11:ll-9]) c.start.month, _ = strconv.parse_int(line[ll-11:ll-9]) if verbose do fmt.printf("Year: %s\n", line[ll-15:ll-11]) c.start.year, _ = strconv.parse_int(line[ll-15:ll-11]) } } if len(line)>=5 { if line[0:5]=="DTEND" { // grab the timestamp from the end of the line, and set end to it ll := len(line) if verbose do fmt.println("Found a DTEND!") if verbose do fmt.println(line) if verbose do fmt.printf("Time: %s:%s\n", line[ll-6:ll-4], line[ll-4:ll-2]) if verbose do fmt.printf("Hours: %s\n", line[ll-6:ll-4]) c.end.hours, _ = strconv.parse_int(line[ll-6:ll-4]) if verbose do fmt.printf("Minutes: %s\n", line[ll-4:ll-2]) c.end.minutes, _ = strconv.parse_int(line[ll-4:ll-2]) if verbose do fmt.printf("Day: %s\n", line[ll-9:ll-7]) c.end.day, _ = strconv.parse_int(line[ll-9:ll-7]) if verbose do fmt.printf("Month: %s\n", line[ll-11:ll-9]) c.end.month, _ = strconv.parse_int(line[ll-11:ll-9]) if verbose do fmt.printf("Year: %s\n", line[ll-15:ll-11]) c.end.year, _ = strconv.parse_int(line[ll-15:ll-11]) } } if line[0:min(8, len(line))]=="SUMMARY:" { if verbose do fmt.println("Found a SUMMARY!") if verbose do fmt.println(line) ll := len(line) c.title = line[8:ll] } // TODO: This is checking if the years are 0 to make sure it hasn't read from // from a line containing "DTSTART;VALUE" instead of "DTSTART;TZID" // VALUE days are events that are set to last the entire day, // as opposed to having a defined start and end point. // // This should eventually not be needed, because these days // should also be imported based on the session's default-day settings if line=="END:VEVENT" && (c.end.year != 0) && (c.start.year != 0) { if verbose do fmt.println(line) c.value = 1 append(&output, c) blank_timeblock: Timeblock c = blank_timeblock if verbose do fmt.println("\n\n", i, line_nr, "\n\n") i += 1 } line_nr += 1 } slice.sort_by(output[:], lessTimeblock) return output, true } // // --- BASIC OPERATIONS --- // add :: proc(moment: Moment, delta: Delta) -> (output: Moment) { output = moment wind(&output, delta) return } sub :: proc(moment: Moment, delta: Delta) -> (output: Moment) { output = moment wind(&output, delta.minutes*-1, delta.hours*-1, delta.days*-1) return } maxMoment :: proc(moment_a: Moment, moment_b: Moment) -> Moment { if sortable(moment_a) > sortable(moment_b) do return moment_a return moment_b } maxDelta :: proc(delta_a: Delta, delta_b: Delta) -> Delta { if sortable(delta_a) > sortable(delta_b) do return delta_a return delta_b } time_max :: proc{maxDelta, maxMoment} minMoment :: proc(moment_a: Moment, moment_b: Moment) -> Moment { if sortable(moment_a) < sortable(moment_b) do return moment_a return moment_b } minDelta :: proc(delta_a: Delta, delta_b: Delta) -> Delta { if sortable(delta_a) < sortable(delta_b) do return delta_a return delta_b } time_min :: proc{minDelta, minMoment} clampMoment :: proc(moment: Moment, moment_min: Moment, moment_max: Moment) -> Moment { return time_min(time_max(moment, moment_min), moment_max) } clampDelta :: proc(delta: Delta, delta_min: Delta, delta_max: Delta) -> Delta { return time_min(time_max(delta, delta_min), delta_max) } time_clamp :: proc{clampMoment, clampDelta} greatMoment :: proc(moment_a: Moment, moment_b: Moment) -> bool { return bool(sortable(moment_a) > sortable(moment_b)) } greatDelta :: proc(delta_a: Delta, delta_b: Delta) -> bool { return bool(sortable(delta_a) > sortable(delta_b)) } great :: proc{greatMoment, greatDelta} lessMoment :: proc(moment_a: Moment, moment_b: Moment) -> bool { return bool(sortable(moment_a) < sortable(moment_b)) } lessDelta :: proc(delta_a: Delta, delta_b: Delta) -> bool { return bool(sortable(delta_a) < sortable(delta_b)) } lessTimeblock :: proc(block_a: Timeblock, block_b: Timeblock) -> bool { if block_b.start == {0, 0, 0, 0, 0} do return true if block_a.start == {0, 0, 0, 0, 0} do return false return bool(sortable(block_a.start) < sortable(block_b.start)) } lessWorkday :: proc(day_a: Workday, day_b: Workday) -> bool { return bool(sortable(day_a.call) < sortable(day_b.call)) } lessWorkdayPtr :: proc(day_a: ^Workday, day_b: ^Workday) -> bool { return bool(sortable(day_a.call) < sortable(day_b.call)) } less :: proc{lessMoment, lessDelta, lessTimeblock, lessWorkday} lessEqMoment :: proc(moment_a: Moment, moment_b: Moment) -> bool { return moment_a==moment_b || less(moment_a, moment_b) } lessEq :: proc{lessEqMoment} greatEqMoment :: proc(moment_a: Moment, moment_b: Moment) -> bool { return moment_a == moment_b || great(moment_a, moment_b) } greatEq :: proc{greatEqMoment} diff :: proc(moment_a: Moment, moment_b: Moment) -> (acc: Delta) { // FIXME: This seems to cause either infinite loops or crashes sometimes // Uses what I call an accumulator-decumulator design // Count how long it takes to approach a benchmark, // and that count is the difference acc = {0, 0, 0} if moment_a == moment_b do return // smallest operand becomes benchmark to approach reverse: bool = sortable(moment_a) < sortable(moment_b) bench : Moment dec : Moment if reverse { bench = moment_a dec = moment_b } else { bench = moment_b dec = moment_a } // It is possible to write something that does this in months at a time, instead of days, // which would be faster, but I am not expecting to have to do this with such // long periods of time, so screw that. for ((dec.year - bench.year) > 1 || (dec.month - bench.month) > 1 || (dec.day - bench.day) > 1) { wind(&dec, 0, 0, -1) acc.days += 1 } for (dec.hours - bench.hours > 1) { wind(&dec, 0, -1, 0) acc.hours += 1 } for acc.hours > 23 { acc.hours -= 24 acc.days += 1 } for dec != bench { wind(&dec, -1, 0, 0) acc.minutes += 1 } for acc.minutes > 59 { acc.minutes -= 60 acc.hours += 1 } // Repeating this is a little bit ugly, but it works for acc.hours > 23 { acc.hours -= 24 acc.days += 1 } return } sortableTimeDelta :: proc(delta: Delta) -> (output: u64) { output, _ = strconv.parse_u64(fmt.tprintf("1%3i%2i%2i", delta.days, delta.hours, delta.minutes)) return } sortableTimeMoment :: proc(moment: Moment) -> (output: u64) { output, _ = strconv.parse_u64(fmt.tprintf("%4i%2i%2i%2i%2i", moment.year, moment.month, moment.day, moment.hours, moment.minutes)) return } sortable :: proc{sortableTimeMoment, sortableTimeDelta} deltaToString :: proc(delta: Delta) -> (output: string) { if delta.hours == 0 && delta.days == 0 && delta.minutes == 0 { return "None" } cat_array : [dynamic]string printed_prev : bool = false if delta.days>0 { buf: [5]byte append(&cat_array, fmt.tprint(delta.days)) if delta.days < 2 { append(&cat_array, " day") } else { append(&cat_array, " days") } printed_prev = true } if delta.hours>0 { if printed_prev do append(&cat_array, ", ") buf: [5]byte append(&cat_array, fmt.tprint(delta.hours)) if delta.hours < 2 { append(&cat_array, " hour") } else { append(&cat_array, " hours") } printed_prev = true } if delta.minutes>0 { if printed_prev do append(&cat_array, ", ") buf: [5]byte append(&cat_array, fmt.tprint(delta.minutes)) if delta.minutes < 2 { append(&cat_array, " minute") } else { append(&cat_array, " minutes") } } output = strings.concatenate(cat_array[:]) return } momentToString :: proc(moment: Moment) -> (output: string) { cat_array: [dynamic]string output = fmt.tprintf("%4i-%2i-%2i %2i:%2i", moment.year, moment.month, moment.day, moment.hours, moment.minutes) return } timeblockToString :: proc(block: Timeblock) -> (output: string) { s: [5]string = {} output = fmt.aprintf("%s -> %02d:%02d | %0.3f hrs | %s", toString(block.start), block.end.hours, block.end.minutes, hourcount(block), block.title) return } toString :: proc{deltaToString, momentToString, timeblockToString} clockprintMoment :: proc(moment: Moment) -> string { return fmt.tprintf("%2i:%2i", moment.hours, moment.minutes) } clockprintTimeblock :: proc(block: Timeblock) -> string { return fmt.tprintf("%s -> %s", clockprint(block.start), clockprint(block.end)) } clockprint :: proc{clockprintTimeblock, clockprintMoment} dayprintMoment :: proc(moment: Moment) -> string { return fmt.tprintf("%4i-%2i-%2i", moment.year, moment.month, moment.day) } dayprintTimeblock :: proc(block: Timeblock) -> string { return fmt.tprintf("%s -> %s", dayprint(block.start), dayprint(block.end)) } dayprint :: proc{dayprintTimeblock, dayprintMoment} getweekday :: proc(moment: Moment) -> Weekday { y: int = moment.year t: []int = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 } y -= int(moment.month < 3) return Weekday((y + y / 4 - y / 100 + y / 400 + t[moment.month - 1] + moment.day - 1) % 7) } hourcount :: proc(block: Timeblock) -> f32 { delta: Delta = diff(block.end, block.start) return f32(f32(delta.minutes)/60 + f32(delta.hours) + f32(delta.days) * 24) } minutecount :: proc(block: Timeblock) -> int { delta: Delta = diff(block.end, block.start) return (delta.minutes) + (delta.hours*60) + (delta.days*24*60) } daycount :: proc(delta: Delta) -> f32 { assert(delta != {0,0,0}) return f32(f32(delta.minutes)/60/24 + f32(delta.hours)/24 + f32(delta.days) ) } days_in :: proc(month: int, year: int) -> int { switch month { case 1: return 31; case 2: if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)){ return 29; } return 28; case 3: return 31; case 4: return 30; case 5: return 31; case 6: return 30; case 7: return 31; case 8: return 31; case 9: return 30; case 10: return 31; case 11: return 30; case 12: return 31; } fmt.printf("You just found month nr: %i. Something is very wrong.\n", month) fmt.assertf(month < 13 && month > 0, "You tried to get the days in month %i!\n", month) return 30 } gaussEaster :: proc(year: int) -> Moment { // Thanks to Carl Friedrich Gauss for the algorythm // Thanks rahulhegde97, bansal_rtk_, code_hunt, sanjoy_62, simranarora5sos // and aashutoshparoha on GeeksForGeeks for the implementation I based this on. A, B, C, P, Q, M, N, D, E: f64 easter_month: int = 0 easter_day: int = 0 A = f64(year % 19) B = f64(year % 4) C = f64(year % 7) P = f64(math.floor(f64(year / 100.0))) Q = math.floor((13 + 8 * P) / 25.0) M = f64(int(15 - Q + P - math.floor(f64(P / 4))) % 30) N = f64(int(4 + P - math.floor(P / 4)) % 7) D = f64(int(19 * A + M) % 30) E = f64(int(2 * B + 4 * C + 6 * D + N) % 7) days: int = int(22 + D + E) easter_day = days if (D == 29) && (E == 6) { // A corner case when D is 29 easter_month = 4 easter_day = 19 } else if (D == 28) && (E == 6) { // Another corner case, when D is 28 easter_month = 4 easter_day = 18 } else { // If days > 31, move to April // April = 4th Month if (days > 31) { easter_month = 04 easter_day = days-31 } else { // Otherwise, stay on March // March = 3rd Month easter_month = 03 } } return {0, 0, easter_day, easter_month, year} }