Compare commits
2 Commits
3aef7c4133
...
6e07d120a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e07d120a2 | ||
|
|
a10d3e1326 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
**/*.csv
|
||||
.ipynb_checkpoints/
|
||||
__pycache__/
|
||||
diet
|
||||
journal.json
|
||||
|
||||
82
foods
82
foods
@@ -1,4 +1,62 @@
|
||||
|
||||
Kaurakeksi
|
||||
Energy 461kcal
|
||||
Fat 20g
|
||||
SaturatedFat 8.5g
|
||||
Carbs 56g
|
||||
Sugar 31g
|
||||
Fiber 4.8g
|
||||
Protein 7.9g
|
||||
Salt 0.2g
|
||||
|
||||
Cereal
|
||||
Energy 373kcal
|
||||
Fat 1.4g
|
||||
SaturatedFat 0.4g
|
||||
Carbs 78g
|
||||
Sugar 13.9g
|
||||
Fiber 7.1g
|
||||
Protein 8.5g
|
||||
Salt 0.39g
|
||||
|
||||
SadonkorjuuPuuro
|
||||
Energy 385kcal
|
||||
Fat 11g
|
||||
SaturatedFat 1.7g
|
||||
Carbs 51g
|
||||
Sugar 1.1g
|
||||
Fiber 11g
|
||||
Protein 14g
|
||||
|
||||
DaimCake
|
||||
Energy 446kcal
|
||||
Fat 28g
|
||||
SaturatedFat 9.3g
|
||||
Carbs 39g
|
||||
Sugar 37g
|
||||
Protein 9.1g
|
||||
Salt 0.35g
|
||||
|
||||
KaneliKierre
|
||||
Energy 350kcal
|
||||
Fat 14g
|
||||
SaturatedFat 5.6g
|
||||
Carbs 55g
|
||||
Sugar 20g
|
||||
Fiber 2g
|
||||
Protein 6g
|
||||
Salt 0.7g
|
||||
|
||||
Munkki
|
||||
Energy 395kcal
|
||||
Fat 23g
|
||||
SaturatedFat 10g
|
||||
Carbs 39g
|
||||
Sugar 9.6g
|
||||
Fiber 2.7g
|
||||
Protein 5.8g
|
||||
Salt 0.5g
|
||||
|
||||
ProteinRakha
|
||||
Energy 67kcal
|
||||
Fat 0.2g
|
||||
@@ -693,6 +751,10 @@ Milk 180g
|
||||
Potato 1000g
|
||||
TOTAL 1300g
|
||||
|
||||
MashedPotato3
|
||||
Potato 1050g
|
||||
Milk 350g
|
||||
|
||||
Blov
|
||||
BrownRice 600g
|
||||
Ketchup 250g
|
||||
@@ -791,6 +853,12 @@ Milk 240g
|
||||
Egg 65g
|
||||
GrahamFlour 60g
|
||||
|
||||
VegeKotlet2
|
||||
HarkisRouheseos 200g
|
||||
Milk 450g
|
||||
WholeGrainBread 50g
|
||||
Flour 90g
|
||||
|
||||
RoastedVeggies2
|
||||
Broccoli 300g
|
||||
Oil 70g
|
||||
@@ -804,9 +872,23 @@ PeasCornPepper 200g
|
||||
Ketchup 100g
|
||||
TOTAL 760g
|
||||
|
||||
BeanitHarkisVeggies2
|
||||
Oil 40g
|
||||
BeanitHarkis 250g
|
||||
PeasCornPepper 220g
|
||||
Ketchup 100g
|
||||
TOTAL 740g
|
||||
|
||||
Pancake
|
||||
SkimMilk 430g
|
||||
Water 300g
|
||||
Oil 30g
|
||||
Flour 430g
|
||||
Sugar 20g
|
||||
|
||||
SadonkorjuuPuuroBoiled
|
||||
SadonkorjuuPuuro 160g
|
||||
SkimMilk 100g
|
||||
Margarine 20g
|
||||
TOTAL 788
|
||||
|
||||
|
||||
224
parse.py
224
parse.py
@@ -1,110 +1,162 @@
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
from typing import Union
|
||||
import sys
|
||||
from datetime import datetime
|
||||
import re
|
||||
import json
|
||||
from functools import reduce
|
||||
|
||||
JOURNAL_PATH = '/Users/olari/workspace/journal'
|
||||
entry_re = re.compile(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', re.MULTILINE)
|
||||
|
||||
entry_re = re.compile(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')
|
||||
curr_day = ''
|
||||
|
||||
def get_daily_file_paths() -> list[Path]:
|
||||
return list(sorted(Path(JOURNAL_PATH).glob('*.md')))
|
||||
|
||||
def resolve_relative_time_expression(now: datetime, expr: str) -> datetime:
|
||||
weekdays = [
|
||||
'monday', 'tuesday', 'wednesday', 'thursday',
|
||||
'friday', 'saturday', 'sunday'
|
||||
]
|
||||
def parse_godword(godword):
|
||||
return godword.split()
|
||||
|
||||
if expr == 'today':
|
||||
return now
|
||||
elif expr == 'tomorrow':
|
||||
return now + timedelta(days=1)
|
||||
elif expr == 'yesterday':
|
||||
return now - timedelta(days=1)
|
||||
elif expr in weekdays:
|
||||
return now - timedelta(days=now.weekday()) + weekdays.index(expr)
|
||||
else:
|
||||
return None
|
||||
|
||||
def parse_godword(content: str) -> list[str]:
|
||||
return content.split()
|
||||
|
||||
def parse_habits(content: str) -> dict[str, bool]:
|
||||
return {
|
||||
line[4:]: line[1] == 'x'
|
||||
for line in content.splitlines()
|
||||
}
|
||||
def parse_habits(habits):
|
||||
result = {}
|
||||
for habit in habits.splitlines():
|
||||
value, name = habit.split(maxsplit=1)
|
||||
name = name.strip()
|
||||
result[name] = value[1] == 'x'
|
||||
return result
|
||||
|
||||
header_modules = {
|
||||
'godword': (None, parse_godword),
|
||||
'habits': (None, parse_habits),
|
||||
'godword': parse_godword,
|
||||
'habits': parse_habits,
|
||||
}
|
||||
|
||||
def parse_header_module(content: str) -> tuple[str, Union[list, dict]]:
|
||||
name, *content = content.splitlines()
|
||||
name = name.removesuffix(':').lower()
|
||||
content = '\n'.join(content)
|
||||
_, parse = header_modules[name]
|
||||
return name, parse(content)
|
||||
|
||||
def parse_header(header):
|
||||
title, *modules = header.split('\n\n')
|
||||
title = title.removeprefix('# ')
|
||||
return {
|
||||
'title': title,
|
||||
'modules': dict(parse_header_module(module) for module in modules)
|
||||
}
|
||||
result = {}
|
||||
|
||||
def parse_diet(content):
|
||||
pass
|
||||
def split_into_blocks(text):
|
||||
return [b.strip() for b in re.split(r'\n{2,}', text) if b.strip() != '']
|
||||
|
||||
def parse_content(content):
|
||||
content = content.strip()
|
||||
return {
|
||||
'blocks': [b.replace('\n', ' ') for b in content.split('\n\n')]
|
||||
}
|
||||
title, *modules = split_into_blocks(header)
|
||||
|
||||
for module in modules:
|
||||
name, value = module.split('\n', maxsplit=1)
|
||||
name = name.lower().removesuffix(':')
|
||||
result[name] = header_modules[name](value)
|
||||
|
||||
return result
|
||||
|
||||
def parse_timestamp(timestamp):
|
||||
return datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S')
|
||||
|
||||
def parse_post(block):
|
||||
block = block.removeprefix('@post ')
|
||||
try:
|
||||
timestamp = int(parse_timestamp(block[:19]).timestamp())
|
||||
block = block[19:]
|
||||
except:
|
||||
timestamp = None
|
||||
content = block
|
||||
return {'timestamp': timestamp, 'content': content}
|
||||
|
||||
def parse_notes(block):
|
||||
tag, source, title = block.splitlines()
|
||||
return {'source': source, 'title': title}
|
||||
|
||||
def parse_diet(block):
|
||||
tag, amount, food = block.split()
|
||||
amount = float(amount.removesuffix('g'))
|
||||
return {'amount': amount, 'food': food}
|
||||
|
||||
def parse_timer(block):
|
||||
tag, *rest = block.split()
|
||||
|
||||
name = None
|
||||
timestamp = None
|
||||
if len(rest) > 2:
|
||||
name, *rest = rest
|
||||
if len(rest) > 1:
|
||||
timestamp = int(parse_timestamp(' '.join(rest)).timestamp())
|
||||
|
||||
result = {}
|
||||
if name:
|
||||
result['name'] = name
|
||||
if timestamp:
|
||||
result['timestamp'] = timestamp
|
||||
return result
|
||||
|
||||
def parse_exercise(block):
|
||||
tag, *parts = block.split()
|
||||
|
||||
if parts[0] == 'walk':
|
||||
kind, minutes, distance, steps = parts
|
||||
return {
|
||||
'kind': kind,
|
||||
'minutes': int(minutes.removesuffix('min')),
|
||||
'distance': float(distance.removesuffix('km')),
|
||||
'steps': int(steps.removesuffix('steps')),
|
||||
}
|
||||
|
||||
return {'kind': 'INVALID'}
|
||||
|
||||
def create_entry_module_parser(name, handler=None):
|
||||
handler = handler or (lambda b: {'value': b.removeprefix(f'@{name} ')})
|
||||
return lambda b: {'type': name} | handler(b)
|
||||
|
||||
entry_modules = {
|
||||
'hide': create_entry_module_parser('hide', lambda _: {}),
|
||||
'post': create_entry_module_parser('post', parse_post),
|
||||
'info': create_entry_module_parser('info'),
|
||||
'notes': create_entry_module_parser('notes', parse_notes),
|
||||
'behavior': create_entry_module_parser('behavior'),
|
||||
'diet': create_entry_module_parser('diet', parse_diet),
|
||||
'task': create_entry_module_parser('task'),
|
||||
'start': create_entry_module_parser('start', parse_timer),
|
||||
'stop': create_entry_module_parser('stop', parse_timer),
|
||||
'done': create_entry_module_parser('done', parse_timer),
|
||||
'exercise': create_entry_module_parser('exercise', parse_exercise),
|
||||
}
|
||||
|
||||
def parse_entry(entry):
|
||||
return {
|
||||
'timestamp': int(parse_timestamp(entry[:19]).timestamp()),
|
||||
'content': parse_content(entry[19:]),
|
||||
}
|
||||
|
||||
def parse_file(fpath):
|
||||
header = {}
|
||||
entries = []
|
||||
|
||||
buf = []
|
||||
|
||||
for i, line in enumerate(fpath.read_text().splitlines()):
|
||||
if entry_re.match(line):
|
||||
if not header:
|
||||
header = parse_header('\n'.join([c.strip() for c in buf]))
|
||||
else:
|
||||
entries.append(parse_entry('\n'.join(buf)))
|
||||
buf = [line]
|
||||
else:
|
||||
buf.append(line)
|
||||
|
||||
return {
|
||||
'header': header,
|
||||
'entries': entries,
|
||||
}
|
||||
|
||||
def parse_journal():
|
||||
result = {}
|
||||
|
||||
for fpath in get_daily_file_paths()[-5:]:
|
||||
info = parse_file(fpath)
|
||||
result[info['header']['title']] = info
|
||||
def split_into_blocks(text):
|
||||
result = []
|
||||
|
||||
for block in re.split(r'\n{2,}', text):
|
||||
block = block.strip()
|
||||
if not block:
|
||||
continue
|
||||
|
||||
for i, module in enumerate(block.replace(' @', '\n@').split('\n@')):
|
||||
#module = module.strip().replace('\n', ' ')
|
||||
if i == 0:
|
||||
result.append(module)
|
||||
else:
|
||||
result.append('@'+module)
|
||||
|
||||
return result
|
||||
|
||||
timestamp, content = entry
|
||||
|
||||
result['timestamp'] = int(parse_timestamp(timestamp.strip()).timestamp())
|
||||
result['blocks'] = []
|
||||
|
||||
for b in split_into_blocks(content):
|
||||
if b[0] == '@':
|
||||
tag = b.split()[0][1:]
|
||||
result['blocks'].append(entry_modules[tag](b))
|
||||
else:
|
||||
result['blocks'].append(b)
|
||||
|
||||
return result
|
||||
|
||||
import json
|
||||
open('journal.json', 'w').write(json.dumps(parse_journal()))
|
||||
result = {}
|
||||
|
||||
for fpath in list(sorted((Path.home() / 'workspace' / 'journal').glob('*.md'))):
|
||||
curr_day = fpath.stem
|
||||
|
||||
header, *tmp = entry_re.split(fpath.read_text())
|
||||
entries = list(zip(tmp[::2], tmp[1::2]))
|
||||
|
||||
result[fpath.stem] = {
|
||||
'header': parse_header(header),
|
||||
'entries': [parse_entry(e) for e in entries],
|
||||
}
|
||||
|
||||
with open('journal.json', 'w') as fp:
|
||||
json.dump(result, fp, indent=4, ensure_ascii=False)
|
||||
|
||||
36
progress.py
36
progress.py
@@ -7,27 +7,11 @@ from common import parse_timestamp
|
||||
|
||||
content = open(sys.argv[1]).read().strip()
|
||||
|
||||
"""
|
||||
|
||||
* Total hours
|
||||
* Hours today
|
||||
* Hours this week
|
||||
|
||||
* Percentage completed
|
||||
* Pages completed
|
||||
|
||||
* Estimated hours to completion
|
||||
* Estimated completion date
|
||||
|
||||
"""
|
||||
|
||||
lines = content.splitlines()
|
||||
i = 0
|
||||
current_chapter = ''
|
||||
result = defaultdict(float)
|
||||
|
||||
total_chapters = 0
|
||||
completed_chapters = 0
|
||||
completed_chapters = set()
|
||||
|
||||
today = datetime.now().replace(hour=0,minute=0,second=0,microsecond=0)
|
||||
this_week = today - timedelta(days=7)
|
||||
@@ -38,13 +22,17 @@ week_hours = 0.0
|
||||
|
||||
oldest_timestamp = datetime.now()
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i].strip()
|
||||
|
||||
if line.startswith('#'):
|
||||
current_chapter = line[line.find(' ')+1:]
|
||||
total_chapters += 1
|
||||
elif line.startswith('@start'):
|
||||
else:
|
||||
completed_chapters.add(current_chapter)
|
||||
|
||||
if line.startswith('@start'):
|
||||
start = parse_timestamp(line.removeprefix('@start '))
|
||||
|
||||
if start < oldest_timestamp:
|
||||
@@ -54,28 +42,18 @@ while i < len(lines):
|
||||
line = lines[i].strip()
|
||||
|
||||
end = parse_timestamp(line.removeprefix('@stop '))
|
||||
|
||||
delta = end - start
|
||||
|
||||
hours = delta.seconds / 60 / 60
|
||||
|
||||
result[current_chapter] += hours
|
||||
total_hours += hours
|
||||
|
||||
if start > this_week:
|
||||
week_hours += hours
|
||||
if start > today:
|
||||
day_hours += hours
|
||||
|
||||
elif line.startswith('@done'):
|
||||
completed_chapters += 1
|
||||
|
||||
|
||||
|
||||
i += 1
|
||||
|
||||
#from pprint import pprint
|
||||
#pprint(dict(result), sort_dicts=False)
|
||||
completed_chapters = len(completed_chapters)
|
||||
|
||||
num_days = (datetime.now() - oldest_timestamp).days or 1
|
||||
hours_per_day = total_hours / num_days
|
||||
|
||||
79
summary.py
79
summary.py
@@ -7,88 +7,19 @@ import string
|
||||
|
||||
import sys
|
||||
|
||||
def parse_foods_file():
|
||||
path = Path.home() / 'projects' / 'open-journal' / 'foods'
|
||||
text = path.read_text()
|
||||
foods, recipes = text.split('---')
|
||||
do_yesterday = len(sys.argv) > 1
|
||||
|
||||
def parse_macro(macro):
|
||||
if macro == '...':
|
||||
return ('INVALID', 0.0)
|
||||
|
||||
name, value = macro.split()
|
||||
value = float(value.removesuffix('g').removesuffix('kcal'))
|
||||
return (name, value)
|
||||
|
||||
foods = {
|
||||
macros[0]: dict(parse_macro(macro) for macro in macros[1:])
|
||||
for macros in [food.split('\n') for food in foods.strip().split('\n\n')]
|
||||
}
|
||||
|
||||
def combine_values(fst, snd):
|
||||
result = fst.copy()
|
||||
for k,v in snd.items():
|
||||
if k in fst:
|
||||
result[k] += v
|
||||
else:
|
||||
result[k] = v
|
||||
return result
|
||||
|
||||
def evaluate_ingredients(ingredients):
|
||||
result = {}
|
||||
|
||||
total_weight = 0.0
|
||||
for ingredient in ingredients:
|
||||
k,v = parse_macro(ingredient)
|
||||
if k == 'TOTAL':
|
||||
result[k] = v
|
||||
break
|
||||
else:
|
||||
total_weight += v
|
||||
|
||||
|
||||
food = foods[k]
|
||||
|
||||
for kk,vv in food.items():
|
||||
if kk not in result:
|
||||
result[kk] = 0.0
|
||||
|
||||
result[kk] += vv * (v/100.0)
|
||||
|
||||
if 'TOTAL' not in result:
|
||||
result['TOTAL'] = total_weight
|
||||
|
||||
return result
|
||||
|
||||
recipes = {
|
||||
ingredients[0]: evaluate_ingredients(ingredients[1:])
|
||||
for ingredients in [
|
||||
recipe.split('\n') for recipe in recipes.strip().split('\n\n')
|
||||
]
|
||||
}
|
||||
|
||||
def get_calories_from_macros(mm):
|
||||
calories = 0.0
|
||||
for k,v in mm.items():
|
||||
calories += v * {
|
||||
'Carbs': 4,
|
||||
'Fat': 9,
|
||||
'Protein': 4
|
||||
}.get(k, 0.0)
|
||||
return calories
|
||||
|
||||
#for k,v in foods.items():
|
||||
# print(round(v.get('Energy') - get_calories_from_macros(v)), k)
|
||||
|
||||
return foods, recipes
|
||||
from common import parse_foods_file
|
||||
|
||||
foods, recipes = parse_foods_file()
|
||||
|
||||
entry_re = re.compile(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ', re.MULTILINE)
|
||||
diet_re = re.compile(r'@diet (\d+g) ([a-zA-Z]+)')
|
||||
|
||||
day_index = -2 if do_yesterday else -1
|
||||
|
||||
current_day = list(sorted((Path.home() / 'workspace' /
|
||||
'journal').glob('*.md')))[-1]
|
||||
'journal').glob('*.md')))[day_index]
|
||||
|
||||
header, *tmp = entry_re.split(current_day.read_text())
|
||||
entries = list(zip(tmp[::2], tmp[1::2]))
|
||||
|
||||
Reference in New Issue
Block a user