Skip to content

Instantly share code, notes, and snippets.

@christiangenco
Last active July 13, 2023 14:47
Show Gist options
  • Select an option

  • Save christiangenco/1098b8acc5a426815226e6a04f23c474 to your computer and use it in GitHub Desktop.

Select an option

Save christiangenco/1098b8acc5a426815226e6a04f23c474 to your computer and use it in GitHub Desktop.

Revisions

  1. christiangenco revised this gist Jul 13, 2023. No changes.
  2. christiangenco created this gist Jul 13, 2023.
    152 changes: 152 additions & 0 deletions snapThings.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,152 @@
    #!/usr/bin/env ruby

    # require 'sqlite3'
    require 'shellwords'
    require 'json'
    require 'csv'
    # require 'pry'

    def tasks_to_csv(sqlite3_path, dest_path)
    query = 'select TASK.*, AREA.title as areaTitle, PROJECT.title as projectTitle, HEADING.title as headingTitle'
    # query = "select *,"
    # query += " strftime('%Y', TASK.stopDate) as stopDateX"
    query += ' from TMTask as TASK'
    query += ' LEFT OUTER JOIN TMTask PROJECT ON TASK.project = PROJECT.uuid'
    query += ' LEFT OUTER JOIN TMArea AREA ON TASK.area = AREA.uuid'
    query += ' LEFT OUTER JOIN TMTask HEADING ON TASK.actionGroup = HEADING.uuid'
    `sqlite3 -header -csv #{sqlite3_path.shellescape} "#{query}" > #{dest_path.shellescape}`
    end

    BASE_DIR = File.expand_path("~/Library/Containers/com.culturedcode.ThingsMac/Data/Library/Application\ Support/Cultured\ Code/Things/")

    puts "translating all the sqlite3 databases into csv snapshots"
    backups_glob_path = File.join(BASE_DIR, 'Backups/*.sqlite3')
    dest_dir = File.join(BASE_DIR, 'csvs')
    # generate from backup any days we're missing
    Dir.glob(backups_glob_path).each do |backup_path|
    backup_path =~ /ThingsBackup (\d{4}\-\d{2}\-\d{2})/
    date = Regexp.last_match(1)
    dest = File.join(dest_dir, date + '.csv')
    `mkdir -p #{File.dirname(dest).shellescape}`
    next if File.exist?(dest)

    tasks_to_csv(backup_path, dest)
    end

    today_dest = File.join(dest_dir, Time.now.strftime("%Y-%m-%d") + '.csv')
    tasks_to_csv(File.join(BASE_DIR, "Things.sqlite3"), today_dest) # unless File.exist?(today_dest)
    # exit
    # THEN: compute task_ids_scheduled and task_ids_completed

    # # csv_filename = File.join(dest_dir, "2018-10-27.csv")

    def read_tasks_csv(csv_filename)
    CSV.new(File.read(csv_filename), headers: true).to_a.map{|task|
    task.to_hash.map{|k, v|
    if(k.include?("Date"))
    # dynamically cast all Date fields to actual dates
    [k, Time.at(v.to_f)]
    else
    [k, v]
    end
    }.to_h
    }
    end

    def today(tasks)
    tasks.select do |t|
    t['trashed'] == '0' && # TASK.$ISNOTTRASHED
    t['status'] == '0' && # TASK.$ISOPEN
    t['type'] == '0' && # TASK.$ISTASK
    t['start'] == '1' && # TASK.$ISSTARTED
    t['startDate'].to_i != 0 && # TASK.startdate is NOT NULL
    true
    end
    end

    def completed(tasks)
    tasks.select do |t|
    t['trashed'] == '0' && # TASK.$ISNOTTRASHED
    t['type'] == '0' && # TASK.$ISTASK
    t['status'] == '3' && # TASK.$ISCOMPLETED
    true
    end
    end

    def finished_on(tasks, date)
    date = Date.parse(date) if date.class == String
    tasks.select do |t|
    t["stopDate"].to_date == date
    end
    end

    json_path = File.join(BASE_DIR, "things.json")
    json = File.exists?(json_path) ? JSON.parse(File.read(json_path)) : {}
    tasks_scheduled_completed = json["tasksScheduledCompleted"] || {}

    csvs_glob_path = File.join(BASE_DIR, 'csvs/*.csv')
    Dir.glob(csvs_glob_path).each{|csv_filename|
    puts "processing #{csv_filename}"
    tasks = read_tasks_csv(csv_filename)

    today_date = Date.parse(File.basename(csv_filename, ".csv"))

    today_tasks = today(tasks)
    completed_tasks = completed(tasks)
    completed_today = finished_on(completed_tasks, today_date)

    today_iso = today_date.strftime("%Y-%m-%d")
    tasks_scheduled_completed[today_iso] ||= {};

    tasks_scheduled_completed[today_iso]["taskIdsScheduled"] = (today_tasks.map{|t| t["uuid"]} + completed_today.map{|t| t["uuid"]}).uniq
    }

    puts "work with the most #recent data to define task_ids_completed"
    tasks = read_tasks_csv(Dir.glob(csvs_glob_path).sort.last)
    completed(tasks).each{|task|
    date_iso = task["stopDate"].strftime("%Y-%m-%d")
    tasks_scheduled_completed[date_iso] ||= {}
    (tasks_scheduled_completed[date_iso]["taskIdsCompleted"] ||= []) << task["uuid"]
    }

    scores = json["scores"] || {}
    tasks_scheduled_completed.each{|date_iso, task_ids|
    task_ids_scheduled = task_ids["taskIdsScheduled"] || []
    task_ids_completed = task_ids["taskIdsCompleted"] || []

    monkey_score = task_ids_completed.length
    if task_ids_scheduled.length.to_f == 0
    # managers don't get points if they don't schedule anything
    manager_score = 0
    else
    manager_score = (task_ids_scheduled.length - (task_ids_scheduled - task_ids_completed).length).to_f / task_ids_scheduled.length.to_f
    end

    scores[date_iso] = {
    monkey: monkey_score,
    manager: manager_score,
    }
    # binding.pry
    }

    json["tasksScheduledCompleted"] = tasks_scheduled_completed
    json["scores"] = scores

    # sanity check
    if json && json["scores"] && json["tasksScheduledCompleted"]
    File.open(json_path, 'w'){|f| f.puts JSON.pretty_generate(json)}
    end

    puts json_path
    today_iso = Date.today.strftime("%Y-%m-%d")
    tasks_scheduled_today = json["tasksScheduledCompleted"][today_iso]["taskIdsScheduled"]
    puts "#{tasks_scheduled_today.length} tasks scheduled today"
    p scores[today_iso]

    # db_path = File.expand_path("~/Library/Containers/com.culturedcode.ThingsMac/Data/Library/Application\ Support/Cultured\ Code/Things/Things.sqlite3")
    # db = SQLite3::Database.new(db_path)
    #
    # db.execute( "select * from TMTask" ) do |row|
    # p row
    # binding.pry
    # end