Skip to content

Commit 9d3b6c8

Browse files
Merge pull request #58 from dvklopfenstein/dev
onvert free text containing a time and optionally a date 100x faster than with requisite module
2 parents 7c41365 + a35f0fc commit 9d3b6c8

File tree

10 files changed

+400
-157
lines changed

10 files changed

+400
-157
lines changed

.timetracker/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
start_*.txt

.timetracker/config

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# TimeTracker project configuration file
2+
3+
project = "timetracker"
4+
5+
[csv]
6+
filename = "./timetracker_timetracker_$USER$.csv"

README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
# Timetracker-csv
2-
[![PyPI - Version](https://img.shields.io/pypi/v/timetracker-csv)](https://pypi.org/project/timetracker-csv)
3-
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.14803225.svg)](https://doi.org/10.5281/zenodo.14803225)
4-
![GitHub License](https://img.shields.io/github/license/dvklopfenstein/timetracker)
1+
<p align="center" style="display:inline">
2+
<h1 align="center">Timetracker-csv</h1>
3+
<h3 align="center">Pandas-friendly time tracking from the CLI, repo by repo</h3>
4+
<h3 align="center">
5+
<img src="https://img.shields.io/pypi/v/timetracker-csv" alt="PyPI - Version"> |
6+
<img src="https://zenodo.org/badge/DOI/10.5281/zenodo.14803225.svg" alt="DOI"> |
7+
<img src="https://img.shields.io/github/license/dvklopfenstein/timetracker" alt="License">
8+
</h3>
9+
</p>
510

6-
Track time spent on multiple projects,
11+
---
12+
13+
* Track time spent on multiple projects,
714
one repo at a time from the [CLI](https://blog.iron.io/pros-and-cons-of-a-command-line-interface)
815

9-
Time is saved in
16+
* Time is saved in
1017
[pandas](https://pandas.pydata.org/pandas-docs/stable/index.html)-friendly
1118
[CSV](https://www.datarisy.com/blog/understanding-csv-files-use-cases-benefits-and-limitations) files.
1219

13-
CSV files for each project can be combined into a single CSV file for analysis and plotting.
20+
* CSV files for each project can be combined into a single CSV file for analysis and plotting.
1421

1522
<p align="center"><img src="https://github.com/dvklopfenstein/timetracker/raw/main/doc/mkdocs/source/images/stopwatch.png" alt="timetracker" width="750"/></p>
1623

doc/thirdparty/harvest_import.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"Date","Client","Project","Task","Notes","Hours","First name","Last name"
2+
2008-08-01,"Iridesco","Harvest","Backend Programming","Recurring invoices",1,"Barry","Hess"
3+
2008-08-02,"Iridesco","Harvest","Backend Programming","CSV import",1.02,"Barry","Hess"

tests/pkgtttest/epochcli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from timetracker.utils import yellow
1212

1313
from timetracker.epoch.cli import run
14-
from timetracker.epoch.epoch import get_dt_from_td
14+
from timetracker.epoch.epoch import _get_dt_from_td
1515

1616

1717
def main(arglist=None):
@@ -22,7 +22,7 @@ def main(arglist=None):
2222

2323
def get_dateutils_answer(elapsed_or_dt, dta, defaultdt=None):
2424
"""Get stop datetime, given a start time and a specific or elapsed time"""
25-
dto = get_dt_from_td(elapsed_or_dt, dta)
25+
dto = _get_dt_from_td(elapsed_or_dt, dta)
2626
if dto is not None:
2727
return dto
2828
try:

tests/pkgtttest/timestrs.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env python3
2+
"""Free text containing datetimes"""
3+
4+
from datetime import datetime
5+
6+
NOW = datetime.today()
7+
8+
TIMESTRS = {
9+
# ========================================================
10+
# ========================================================
11+
'12am':{
12+
'exp_dct':{'hour': 0},
13+
'dt': datetime(NOW.year, NOW.month, NOW.day, 0, 0, 0)
14+
},
15+
16+
'12:01am':{
17+
'exp_dct':{'hour': 0, 'minute':1},
18+
'dt': datetime(NOW.year, NOW.month, NOW.day, 0, 1, 0)
19+
},
20+
21+
'12:15:30am':{
22+
'exp_dct':{'hour': 0, 'minute':15, 'second':30},
23+
'dt': datetime(NOW.year, NOW.month, NOW.day, 0, 15, 30)
24+
},
25+
26+
# --------------------------------------------------------
27+
'12pm':{
28+
'exp_dct':{'hour': 12},
29+
'dt': datetime(NOW.year, NOW.month, NOW.day, 12, 0, 0)
30+
},
31+
32+
'12:01pm':{
33+
'exp_dct':{'hour': 12, 'minute':1},
34+
'dt': datetime(NOW.year, NOW.month, NOW.day, 12, 1, 0)
35+
},
36+
37+
'12:45:15pm':{
38+
'exp_dct':{'hour': 12, 'minute':45, 'second':15},
39+
'dt': datetime(NOW.year, NOW.month, NOW.day, 12, 45, 15)
40+
},
41+
42+
# --------------------------------------------------------
43+
'12':{
44+
'exp_dct':{'hour': 12},
45+
'dt': datetime(NOW.year, NOW.month, NOW.day, 12, 0, 0)
46+
},
47+
48+
'12:01':{
49+
'exp_dct':{'hour': 12, 'minute':1},
50+
'dt': datetime(NOW.year, NOW.month, NOW.day, 12, 1, 0)
51+
},
52+
53+
'12:15:30':{
54+
'exp_dct':{'hour': 12, 'minute':15, 'second':30},
55+
'dt': datetime(NOW.year, NOW.month, NOW.day, 12, 15, 30)
56+
},
57+
58+
# --------------------------------------------------------
59+
'13':{
60+
'exp_dct':{'hour': 13},
61+
'dt': datetime(NOW.year, NOW.month, NOW.day, 13, 0, 0)
62+
},
63+
64+
'13:01':{
65+
'exp_dct':{'hour': 13, 'minute':1},
66+
'dt': datetime(NOW.year, NOW.month, NOW.day, 13, 1, 0)
67+
},
68+
69+
'13:45:15':{
70+
'exp_dct':{'hour': 13, 'minute':45, 'second':15},
71+
'dt': datetime(NOW.year, NOW.month, NOW.day, 13, 45, 15)
72+
},
73+
74+
# ========================================================
75+
# ========================================================
76+
'2025-01-02 12am':{
77+
'exp_dct':{'year': 2025, 'month': 1, 'day': 2, 'hour': 0},
78+
'dt': datetime(2025, 1, 2, 0, 0, 0)
79+
},
80+
81+
'01-02 12:01am':{
82+
'exp_dct':{'month': 1, 'day': 2, 'hour': 0, 'minute':1},
83+
'dt': datetime(NOW.year, 1, 2, 0, 1, 0)
84+
},
85+
86+
'1/2 12:01am':{
87+
'exp_dct':{'month': 1, 'day': 2, 'hour': 0, 'minute':1},
88+
'dt': datetime(NOW.year, 1, 2, 0, 1, 0)
89+
},
90+
91+
'13/2 12:01am':{
92+
'exp_dct':None,
93+
'dt': None
94+
},
95+
96+
'5pm':{
97+
'exp_dct':{'hour':17},
98+
'dt': datetime(NOW.year, NOW.month, NOW.day, 17, 0, 0)
99+
},
100+
101+
'13:23:00':{
102+
'exp_dct':{'hour':13, 'minute': 23, 'second': 0},
103+
'dt': datetime(NOW.year, NOW.month, NOW.day, 13, 23, 0),
104+
},
105+
106+
'2025':{
107+
'exp_dct': None,
108+
'dt': None,
109+
},
110+
111+
'2025-06-10 08:57:12 AM':{
112+
'exp_dct':{'year': 2025, 'month': 6, 'day': 10,
113+
'hour': 8, 'minute': 57, 'second': 12},
114+
'dt': datetime(2025, 6, 10, 8, 57, 12),
115+
},
116+
117+
# ========================================================
118+
# ========================================================
119+
'06-10 08:57:12 AM':{
120+
'exp_dct':{'month': 6, 'day': 10,
121+
'hour': 8, 'minute': 57, 'second': 12},
122+
'dt': datetime(2025, 6, 10, 8, 57, 12),
123+
},
124+
125+
'6-10 08:57:12 AM':{
126+
'exp_dct':{'month': 6, 'day': 10,
127+
'hour': 8, 'minute': 57, 'second': 12},
128+
'dt': datetime(2025, 6, 10, 8, 57, 12),
129+
},
130+
131+
# timetracker-csv requires a value for an hour
132+
'2025-06-10':{'exp_dct': None, 'dt': None},
133+
134+
'2025-06-10 8:57:12am':{
135+
'exp_dct':{'year': 2025, 'month': 6, 'day': 10,
136+
'hour': 8, 'minute': 57, 'second': 12},
137+
'dt': datetime(2025, 6, 10, 8, 57, 12),
138+
},
139+
140+
# ========================================================
141+
'13pm':{
142+
'exp_dct': None,
143+
'dt': None,
144+
},
145+
146+
'5pm 3am':{
147+
'exp_dct':{'hour':3},
148+
'dt': datetime(NOW.year, NOW.month, NOW.day, 3, 0, 0),
149+
},
150+
151+
'5pm a 3am a ':{
152+
'exp_dct':{'hour':3},
153+
'dt': datetime(NOW.year, NOW.month, NOW.day, 3, 0, 0),
154+
},
155+
'5pm xtra txt':{
156+
'exp_dct':{'hour':17},
157+
'dt': datetime(NOW.year, NOW.month, NOW.day, 17, 0, 0),
158+
},
159+
160+
'ampm':{'exp_dct':None, 'dt':None},
161+
162+
}

tests/test_stmch.py

Lines changed: 20 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,37 @@
11
#!/usr/bin/env python3
2-
"""Test state machines used for finding datetime in free text"""
2+
"""Test converting free text to datetimes"""
33

4-
from datetime import datetime
54
from datetime import timedelta
65
from timeit import default_timer
76
from timetracker.utils import white
87
from timetracker.epoch.stmach import _SmHhSsAm
9-
from timetracker.epoch.epoch import get_dt_ampm
8+
from timetracker.epoch.stmach import search_texttime
9+
from timetracker.epoch.epoch import _get_dt_ampm
10+
from tests.pkgtttest.timestrs import TIMESTRS
1011

11-
NOW = datetime.today()
1212

13-
TESTIO = {
14-
'12am':{
15-
'exp_dct':[{'hour': 12, 'AM/PM': 'AM'}],
16-
'dt': datetime(NOW.year, NOW.month, NOW.day, 0, 0, 0)
17-
},
18-
19-
'12pm':{
20-
'exp_dct':[{'hour': 12, 'AM/PM': 'PM'}],
21-
'dt': datetime(NOW.year, NOW.month, NOW.day, 12, 0, 0)
22-
},
23-
24-
'12:01am':{
25-
'exp_dct':[{'hour': 12, 'AM/PM': 'AM'}],
26-
'dt': datetime(NOW.year, NOW.month, NOW.day, 0, 1, 0)
27-
},
28-
29-
'12:01pm':{
30-
'exp_dct':[{'hour': 12, 'AM/PM': 'PM'}],
31-
'dt': datetime(NOW.year, NOW.month, NOW.day, 12, 1, 0)
32-
},
33-
34-
'13pm':{
35-
'exp_dct':[{'hour': 13, 'AM/PM': 'PM'}],
36-
'dt': None,
37-
},
38-
39-
'5pm':{
40-
'exp_dct':[{'hour':5, 'AM/PM':'PM'}],
41-
'dt': datetime(NOW.year, NOW.month, NOW.day, 17, 0, 0)
42-
},
43-
44-
'5pm 3am':{
45-
'exp_dct':[{'hour':5, 'AM/PM':'PM'},
46-
{'hour':3, 'AM/PM':'AM'}],
47-
'dt': datetime(NOW.year, NOW.month, NOW.day, 3, 0, 0),
48-
},
49-
50-
'5pm a 3am a ':{
51-
'exp_dct':[{'hour':5, 'AM/PM':'PM'},
52-
{'hour':3, 'AM/PM':'AM'}],
53-
'dt': datetime(NOW.year, NOW.month, NOW.day, 3, 0, 0),
54-
},
55-
56-
'5pm xtra txt':{
57-
'exp_dct':[{'hour':5, 'AM/PM':'PM'}],
58-
'dt': datetime(NOW.year, NOW.month, NOW.day, 17, 0, 0),
59-
},
60-
61-
'ampm':{'exp_dct':[], 'dt':None},
62-
63-
'13:23:00':{
64-
'exp_dct':[{'hour':13, 'minute': 23, 'second': 0}],
65-
'dt': datetime(NOW.year, NOW.month, NOW.day, 13, 23, 0),
66-
},
67-
68-
'2025':{
69-
'exp_dct':[{'year': 2025}],
70-
'dt': None,
71-
},
72-
73-
'2025-06-10 08:57:12 AM':{
74-
'exp_dct':[{'year': 2025, 'month': 6, 'day': 10},
75-
{'hour': 8, 'minute': 57, 'second': 12, 'AM/PM':'AM'}],
76-
'dt': datetime(2025, 6, 10, 8, 57, 12),
77-
},
78-
79-
'2025-06-10 8:57:12am':{
80-
'exp_dct':[{'year': 2025, 'month': 6, 'day': 10},
81-
{'hour': 8, 'minute': 57, 'second': 12, 'AM/PM':'AM'}],
82-
'dt': datetime(2025, 6, 10, 8, 57, 12),
83-
},
84-
}
85-
86-
def test_ampm():
13+
def test_txt_to_dt():
8714
"""Test state machines used for finding 'am', 'pm', 'AM', or 'PM' in free text"""
88-
for txt, dct in TESTIO.items():
15+
for txt, dct in TIMESTRS.items():
8916
#_run_ampm(txt, dct['exp_dct'])
90-
act = get_dt_ampm(txt, None)
17+
act = _get_dt_ampm(txt, None)
9118
assert act == dct['dt'], ('EXP != ACT:\n'
9219
f' TXT: {txt}\n'
9320
f' EXP: {dct["dt"]}\n'
9421
f' ACT: {act}\n')
9522
print(f'{act} <- {txt}\n')
9623

24+
def test_txt_to_dct():
25+
"""Test state machines used for finding 'am', 'pm', 'AM', or 'PM' in free text"""
26+
for txt, dct in TIMESTRS.items():
27+
#_run_ampm(txt, dct['exp_dct'])
28+
act = search_texttime(txt)
29+
assert act == dct['exp_dct'], ('EXP != ACT:\n'
30+
f' TXT: {txt}\n'
31+
f' EXP: {dct["exp_dct"]}\n'
32+
f' ACT: {act}\n')
33+
print(f'{act} <- {txt}\n')
34+
9735

9836
# ------------------------------------------------------------------------
9937
def _run_ampm(txt, exp):
@@ -121,5 +59,6 @@ def _search_for_ampm(txt):
12159

12260
if __name__ == '__main__':
12361
tic_all = default_timer()
124-
test_ampm()
62+
test_txt_to_dt()
63+
test_txt_to_dct()
12564
print(white(f'{timedelta(seconds=default_timer()-tic_all)} TOTAL TIME FOR ALL TESTS'))

0 commit comments

Comments
 (0)