aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--build.bat1
-rw-r--r--logo.webpbin0 -> 29724 bytes
-rw-r--r--main.odin171
-rw-r--r--test-data.ics153
5 files changed, 323 insertions, 6 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..51fa235
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+![logo](logo.webp)
+
+I made statics because I wanted a more convenient way to count the work hours I had tracked in my calendar.
+You may find it useful. \ No newline at end of file
diff --git a/build.bat b/build.bat
new file mode 100644
index 0000000..e50f118
--- /dev/null
+++ b/build.bat
@@ -0,0 +1 @@
+odin run . -- "test-data.ics" -i 2025-08 -v \ No newline at end of file
diff --git a/logo.webp b/logo.webp
new file mode 100644
index 0000000..d0da4d9
--- /dev/null
+++ b/logo.webp
Binary files differ
diff --git a/main.odin b/main.odin
index da72be7..7f05efe 100644
--- a/main.odin
+++ b/main.odin
@@ -2,29 +2,148 @@ package main
import "core:fmt"
import "core:os"
+import "core:strings"
+import "core:strconv"
dayrate : f64 = 3500
+Arg_Type :: enum {
+ NONE,
+ FROM,
+ TO,
+ IN,
+}
+Arg_Flags :: bit_set[Arg_Type]
+
+Range_Flags_Enum :: enum {
+ YEAR,
+ MONTH,
+ DAY,
+ HOUR,
+ MINUTE,
+}
+Range_Flags :: bit_set[Range_Flags_Enum]
+
+Filter :: struct {
+ ranges : Range_Flags,
+ time : Moment,
+}
+
main :: proc() {
arg_count := len(os.args)-1
+ filters : Arg_Flags = {}
+ parsing : Arg_Type = .NONE
+
+ verbose := false
+
+ to_filter : Filter
+ from_filter : Filter
+ in_filter : Filter
+ in_filter_out : Filter
+
+ file_index_buffer : [dynamic]int
+
+ for &arg, i in os.args {
+ switch parsing {
+ case .NONE:
+ lower := strings.to_lower(arg, context.temp_allocator)
+ if lower[max(0, len(lower)-4):len(lower)] == ".ics" {
+ append(&file_index_buffer, i)
+ } else {
+ switch lower {
+ case "-t":
+ parsing = .TO
+ case "-f":
+ parsing = .FROM
+ case "-i":
+ parsing = .IN
+ case "-v":
+ verbose = true
+ case "-h":
+ fmt.println("statICS by Sander J. Skjegstad\n")
+
+ fmt.println("Usage:")
+ fmt.println("statics path/to/file.ics path/to/another_file.ics [FLAGS]")
+
+ fmt.println("\nFlags:")
+ fmt.println("\t-t To: Filter up to and NOT including a specific time.")
+ fmt.println("\t-f From: Filter from and including a specific time.")
+ fmt.println("\t-i In: Filter to inside a specific year, month, etc.")
+ fmt.println("\t-v Verbose: Prints more info.")
+ fmt.println("\t-h Help: Show this screen.")
+
+ fmt.println("\nFilter syntax:")
+ fmt.println("Filters currently only filter based on the event start time.")
+ fmt.println("Specify only as much as you want to filter by.")
+ fmt.println("YYYY-MM-DD-Hr-Mn")
+ fmt.println("\n-t 2015-12-31-12-45 will not count anything from that point and out.")
+ fmt.println("-i 2018-12 will only count things within December of 2018.")
+ os.exit(0)
+ }
+ }
+ case .FROM:
+ parse_to_filter(&arg, &from_filter)
+ filters |= {.FROM}
+ fmt.println("FROM filter set up from", momentToString(from_filter.time))
+ parsing = .NONE
+ case .TO:
+ parse_to_filter(&arg, &to_filter)
+ filters |= {.TO}
+ fmt.println("TO filter set up to", momentToString(to_filter.time))
+ filter_maxx(&to_filter)
+ parsing = .NONE
+ case .IN:
+ parse_to_filter(&arg, &in_filter)
+ in_filter_out = in_filter
+ filter_maxx(&in_filter_out)
+ filters |= {.IN}
+ fmt.println("IN filter set up from", momentToString(in_filter.time), "to", momentToString(in_filter_out.time))
+ parsing = .NONE
+
+ }
+ }
+
total_hours : f64 = 0;
- for i in 1..=arg_count {
+
+ fmt.println()
+
+ for i in file_index_buffer {
fmt.printf("%d: ", i)
+ fmt.println(os.args[i])
timeblocks, ok := importICS(os.args[i])
if ok {
hours : f64 = 0
for each_block in timeblocks {
+ if verbose do fmt.println("Timeblock:", timeblockToString(each_block))
+ pass := true
+ if .FROM in filters {
+ pass_from := greatEq(each_block.start, from_filter.time)
+ if verbose do if !pass_from do fmt.println(" FILTERED! By From filter")
+ pass &= pass_from
+ }
+ if .TO in filters {
+ pass_to := lessEq(each_block.start, to_filter.time)
+ if verbose do if !pass_to do fmt.println(" FILTERED! By To filter")
+ pass &= pass_to
+ }
+ if .IN in filters {
+ pass_in := lessEq(in_filter.time, each_block.start) && lessEq(each_block.start, in_filter_out.time)
+ if verbose do if !pass_in do fmt.println(" FILTERED! By In filter: ")
+ pass &= pass_in
+ }
+
+ if !pass do continue
+
hours += f64(hourcount(each_block))
}
minutes := int(f64(hours-f64(int(hours)))*60.0)
- fmt.println(os.args[i])
- fmt.printf(" Hour count: %f\nHours & Minutes: %02d:%02d\n\n",
- hours, int(hours), minutes)
+ fmt.printf(" Hour count: %f\nHours & Minutes: %02d:%02d\n\n",
+ hours, int(hours), minutes)
total_hours += hours
} else {
// Noffin i guess
@@ -38,8 +157,48 @@ main :: proc() {
total_minutes := int(total_final_hour_fraction*60.0)
- fmt.printf(" Hour count: %f\nHours & Minutes: %02d:%02d\n",
- total_hours, int(total_hours), total_minutes)
+ fmt.printf(" Hour count: %f\nHours & Minutes: %02d:%02d\n",
+ total_hours, int(total_hours), total_minutes)
return
}
+
+
+parse_to_filter :: proc(input : ^string, filter : ^Filter) {
+ i : int = 0
+ ok : bool
+ for substring in strings.split_iterator(input, "-") {
+ switch i {
+ case 0:
+ filter.time.year, ok = strconv.parse_int(substring)
+ if !ok {fmt.eprintln("ERROR: Failed to parse year:", substring)}
+ filter.ranges |= {.YEAR}
+ case 1:
+ filter.time.month, ok = strconv.parse_int(substring)
+ if !ok {fmt.eprintln("ERROR: Failed to parse month:", substring)}
+ filter.ranges |= {.MONTH}
+ case 2:
+ filter.time.day, ok = strconv.parse_int(substring)
+ if !ok {fmt.eprintln("ERROR: Failed to parse day:", substring)}
+ filter.ranges |= {.DAY}
+ case 3:
+ filter.time.hours, ok = strconv.parse_int(substring)
+ if !ok {fmt.eprintln("ERROR: Failed to parse hours:", substring)}
+ filter.ranges |= {.HOUR}
+ case 4:
+ filter.time.minutes, ok = strconv.parse_int(substring)
+ if !ok {fmt.eprintln("ERROR: Failed to parse minutes:", substring)}
+ filter.ranges |= {.MINUTE}
+ }
+ i += 1
+ if !ok do os.exit(1)
+ }
+}
+
+filter_maxx :: proc(filter : ^Filter) {
+ // Super evil retarded bad hack, but it works!
+ if .MONTH not_in filter.ranges { filter.time.month = 99 }
+ if .DAY not_in filter.ranges { filter.time.day = 99 }
+ if .HOUR not_in filter.ranges { filter.time.hours = 99 }
+ if .MINUTE not_in filter.ranges { filter.time.minutes = 99 }
+} \ No newline at end of file
diff --git a/test-data.ics b/test-data.ics
new file mode 100644
index 0000000..97dc5e8
--- /dev/null
+++ b/test-data.ics
@@ -0,0 +1,153 @@
+BEGIN:VCALENDAR
+PRODID:-//Proton AG//WebCalendar 5.0.285.3.a//EN
+VERSION:2.0
+METHOD:PUBLISH
+CALSCALE:GREGORIAN
+X-WR-TIMEZONE:Europe/Oslo
+X-WR-CALNAME:Testdata
+BEGIN:VTIMEZONE
+TZID:Europe/Oslo
+LAST-MODIFIED:20250218T131521Z
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZNAME:CEST
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZNAME:CET
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:asdfasdfasdf-c96@proton.me
+DTSTAMP:20250809T003314Z
+DTSTART;TZID=Europe/Oslo:20250809T001000
+DTEND;TZID=Europe/Oslo:20250809T022500
+SEQUENCE:0
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf@proton.me
+DTSTAMP:20250811T003901Z
+DTSTART;TZID=Europe/Oslo:20250811T020000
+DTEND;TZID=Europe/Oslo:20250811T023000
+SEQUENCE:0
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf-@proton.me
+DTSTAMP:20250811T110447Z
+DTSTART;TZID=Europe/Oslo:20250811T123000
+DTEND;TZID=Europe/Oslo:20250811T130000
+SEQUENCE:0
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf@proton.me
+DTSTAMP:20250812T215300Z
+DTSTART;TZID=Europe/Oslo:20250812T220000
+DTEND;TZID=Europe/Oslo:20250812T233000
+SEQUENCE:0
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf@proton.me
+DTSTAMP:20250814T022625Z
+DTSTART;TZID=Europe/Oslo:20250814T025000
+DTEND;TZID=Europe/Oslo:20250814T043000
+SEQUENCE:0
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf@proton.me
+DTSTAMP:20250821T113931Z
+DTSTART;TZID=Europe/Oslo:20250821T122000
+DTEND;TZID=Europe/Oslo:20250821T132000
+SEQUENCE:1
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf-xp89_@proton.me
+DTSTAMP:20250821T150315Z
+DTSTART;TZID=Europe/Oslo:20250821T150000
+DTEND;TZID=Europe/Oslo:20250821T164500
+SEQUENCE:2
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf@proton.me
+DTSTAMP:20250821T173406Z
+DTSTART;TZID=Europe/Oslo:20250821T182000
+DTEND;TZID=Europe/Oslo:20250821T193000
+SEQUENCE:1
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf@proton.me
+DTSTAMP:20250901T203735Z
+DTSTART;VALUE=DATE:20250830
+SEQUENCE:0
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf-SxVgL1EGmI4R_m-7H@proton.me
+DTSTAMP:20250908T183647Z
+DTSTART;TZID=Europe/Oslo:20250908T201000
+DTEND;TZID=Europe/Oslo:20250908T203000
+SEQUENCE:1
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf-ZxwY84NABQKu3OCcft72RI@proton.me
+DTSTAMP:20250910T150610Z
+DTSTART;TZID=Europe/Oslo:20250910T153000
+DTEND;TZID=Europe/Oslo:20250910T170000
+SEQUENCE:0
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf-OebGFCO4_@proton.me
+DTSTAMP:20250909T215458Z
+DTSTART;TZID=Europe/Oslo:20250909T163000
+DTEND;TZID=Europe/Oslo:20250909T171500
+SEQUENCE:1
+DESCRIPTION:Check start
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf@proton.me
+DTSTAMP:20250909T220109Z
+DTSTART;TZID=Europe/Oslo:20250909T210000
+DTEND;TZID=Europe/Oslo:20250909T220000
+SEQUENCE:0
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+BEGIN:VEVENT
+UID:asdfasdfasdf-XpmbkdnuywoLF@proton.me
+DTSTAMP:20250912T222527Z
+DTSTART;TZID=Europe/Oslo:20250912T020000
+DTEND;TZID=Europe/Oslo:20250912T053000
+SEQUENCE:0
+SUMMARY:Test
+STATUS:CONFIRMED
+END:VEVENT
+END:VCALENDAR \ No newline at end of file