Skip to content

Adding Reminder time #133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified tests/main.sqlite
Binary file not shown.
Binary file modified tests/main.sqlite-shm
Binary file not shown.
Binary file modified tests/main.sqlite-wal
Binary file not shown.
28 changes: 20 additions & 8 deletions tests/test_things.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
import contextlib
import io
import os
import sqlite3
import time
import tracemalloc
import sqlite3
import unittest
import unittest.mock

import things
import things.database


tracemalloc.start()

TEST_DATABASE_FILEPATH = "tests/main.sqlite"
Expand Down Expand Up @@ -357,21 +358,27 @@ def test_tasks_stopdate_timezones(self):

# make sure we get back both tasks completed for date by midnight UTC+5
# change timezone to Pakistan
os.environ['TZ'] = 'UTC-5' # UTC+5, per https://unix.stackexchange.com/a/104091
os.environ["TZ"] = "UTC-5" # UTC+5, per https://unix.stackexchange.com/a/104091
time.tzset()
tasks = things.tasks(stop_date="2024-06-18", status="completed", count_only=True)
tasks = things.tasks(
stop_date="2024-06-18", status="completed", count_only=True
)
self.assertEqual(tasks, 2)

# make sure we get back one task completed for date by midnight UTC
os.environ['TZ'] = 'UTC'
os.environ["TZ"] = "UTC"
time.tzset()
tasks = things.tasks(stop_date="2024-06-18", status="completed", count_only=True)
tasks = things.tasks(
stop_date="2024-06-18", status="completed", count_only=True
)
self.assertEqual(tasks, 1)

# change timezone to New York
os.environ['TZ'] = 'UTC+5' # UTC-5, per https://unix.stackexchange.com/a/104091
os.environ["TZ"] = "UTC+5" # UTC-5, per https://unix.stackexchange.com/a/104091
time.tzset()
tasks = things.tasks(stop_date="2024-06-18", status="completed", count_only=True)
tasks = things.tasks(
stop_date="2024-06-18", status="completed", count_only=True
)
self.assertEqual(tasks, 0)

def test_database_details(self):
Expand Down Expand Up @@ -406,11 +413,16 @@ def test_thingsdate(self):
self.assertEqual("AND deadline == 132464128", sqlfilter)
sqlfilter = things.database.make_unixtime_filter("stopDate", "future")
self.assertEqual(
"AND date(stopDate, 'unixepoch', 'localtime') > date('now', 'localtime')", sqlfilter
"AND date(stopDate, 'unixepoch', 'localtime') > date('now', 'localtime')",
sqlfilter,
)
sqlfilter = things.database.make_unixtime_filter("stopDate", False)
self.assertEqual("AND stopDate IS NULL", sqlfilter)

def test_thingstime(self):
test_task = things.tasks("7F4vqUNiTvGKaCUfv5pqYG")
self.assertEqual(test_task.get("reminder_time"), "12:34")


if __name__ == "__main__":
unittest.main()
Expand Down
56 changes: 53 additions & 3 deletions things/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import plistlib
import re
import sqlite3
import weakref
from textwrap import dedent
from typing import Optional, Union
import weakref


# --------------------------------------------------
Expand Down Expand Up @@ -75,6 +75,7 @@
"heading_title",
"project",
"project_title",
"reminder_time",
"trashed",
"tags",
)
Expand Down Expand Up @@ -107,6 +108,8 @@
# See `convert_isodate_sql_expression_to_thingsdate` for details.
DATE_DEADLINE = "deadline" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary
DATE_START = "startDate" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary
# See 'convert_thingstime_sql_expression_to_isotime' for details.
REMINDER_TIME = "reminderTime" # INTEGER: hhhhhmmmmmm00000000000000000000, in binary

# --------------------------------------------------
# Various filters
Expand Down Expand Up @@ -528,6 +531,9 @@ def make_tasks_sql_query(where_predicate=None, order_predicate=None):
deadline_expression = convert_thingsdate_sql_expression_to_isodate(
f"TASK.{DATE_DEADLINE}"
)
reminder_time_expression = convert_thingstime_sql_expression_to_isotime(
f"TASK.{REMINDER_TIME}"
)

return f"""
SELECT DISTINCT
Expand Down Expand Up @@ -576,8 +582,9 @@ def make_tasks_sql_query(where_predicate=None, order_predicate=None):
CASE
WHEN CHECKLIST_ITEM.uuid IS NOT NULL THEN 1
END AS checklist,
date({start_date_expression}) AS start_date,
date({deadline_expression}) AS deadline,
{start_date_expression} AS start_date,
{deadline_expression} AS deadline,
{reminder_time_expression} AS "reminder_time",
datetime(TASK.{DATE_STOP}, "unixepoch", "localtime") AS "stop_date",
datetime(TASK.{DATE_CREATED}, "unixepoch", "localtime") AS created,
datetime(TASK.{DATE_MODIFIED}, "unixepoch", "localtime") AS modified,
Expand Down Expand Up @@ -703,6 +710,49 @@ def convert_thingsdate_sql_expression_to_isodate(sql_expression):
return f"CASE WHEN {thingsdate} THEN {isodate} ELSE {thingsdate} END"


def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str:
"""
Return SQL Expression that decodes a Things time as a string.

A _Things time_ is an integer where the binary digits are
hhhhhmmmmmm00000000000000000000; h is hours, m is minutes.
Seconds are not encoded in a Things time.

For example, the ISO 8601 time '12:34:00' corresponds to the Things
time 840957952 as integer; in binary that is:
0110010001000000000000000000000
hhhhhmmmmmm00000000000000000000
12 34 00

Parameters
----------
sql_expression : str
A sql expression pointing to a "Things time" integer
in format hhhhhmmmmmm00000000000000000000, in binary.

Example
-------
>>> convert_thingstime_sql_expression_to_isotime('840957952')
"CASE WHEN 840957952 THEN \
printf('%02d:%02d', (840957952 & 2080374784) >> 26, \
(840957952 & 66060288) >> 20) ELSE 840957952 END"
>>> convert_thingstime_sql_expression_to_isotime('reminderTime')
"CASE WHEN reminderTime THEN \
printf('%02d:%02d', (reminderTime & 2080374784) >> 26, \
(reminderTime & 66060288) >> 20) ELSE reminderTime END"
"""
h_mask = 0b1111100000000000000000000000000
m_mask = 0b0000011111100000000000000000000

thingstime = sql_expression
hours = f"({thingstime} & {h_mask}) >> 26"
minutes = f"({thingstime} & {m_mask}) >> 20"

isotime = f"printf('%02d:%02d', {hours}, {minutes})"
# when thingstime is NULL, return thingstime as-is
return f"CASE WHEN {thingstime} THEN {isotime} ELSE {thingstime} END"


def dict_factory(cursor, row):
"""
Convert SQL result into a dictionary.
Expand Down