選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

app.cpp 16 KiB

2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
2週間前
2週間前
1週間前
1週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
1週間前
2週間前
1週間前
2週間前
1週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
1週間前
1週間前
2週間前
1週間前
2週間前
1週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
1週間前
1週間前
2週間前
2週間前
1週間前
2週間前
2週間前
1週間前
2週間前
2週間前
2週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
1週間前
1週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
2週間前
1週間前
2週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. #include <math.h>
  2. #include <memory.h>
  3. #include <time.h>
  4. #include "./djstdlib/core.cpp"
  5. const string LOG_FILE_LOCATION = "./log.gtl"_s;
  6. const string DB_FILE_LOCATION = "./db.gtd"_s;
  7. struct GymLogDbHeader {
  8. uint32 nextId;
  9. };
  10. struct GymLogDbEntry {
  11. uint32 id;
  12. uint32 nameLength;
  13. };
  14. struct GymLogDbParsedEntry {
  15. uint32 id;
  16. string name;
  17. };
  18. struct GymLogDbParsed {
  19. GymLogDbHeader header;
  20. list<GymLogDbParsedEntry> entries;
  21. };
  22. struct WeightRepsInfo {
  23. uint8 reps;
  24. real32 weight;
  25. };
  26. struct GymLogEntry {
  27. uint64 timestamp;
  28. uint32 exerciseId;
  29. union {
  30. WeightRepsInfo weightRepsInfo;
  31. };
  32. };
  33. GymLogDbParsed *parseDb(Arena *arena, string database) {
  34. GymLogDbParsed *dbParsed = PushStruct(arena, GymLogDbParsed);
  35. dbParsed->header = *((GymLogDbHeader *)database.str);
  36. size_t head = sizeof(GymLogDbHeader);
  37. uint32 entriesLeft = dbParsed->header.nextId - 1;
  38. dbParsed->entries = PushList(arena, GymLogDbParsedEntry, entriesLeft);
  39. while (entriesLeft > 0 && head < database.length) {
  40. GymLogDbEntry *currentEntry = (GymLogDbEntry *)((byte *)database.str + head);
  41. GymLogDbParsedEntry parsedEntry = { currentEntry->id, PushString(arena, currentEntry->nameLength) };
  42. head += sizeof(GymLogDbEntry);
  43. memcpy(parsedEntry.name.str, database.str + head, currentEntry->nameLength);
  44. appendList(&dbParsed->entries, parsedEntry);
  45. head += currentEntry->nameLength;
  46. }
  47. return dbParsed;
  48. }
  49. list<GymLogEntry> loadEntryLog(Arena *arena, string fileLocation) {
  50. list<GymLogEntry> result = {0};
  51. string logfile = os_readEntireFile(arena, LOG_FILE_LOCATION);
  52. if (logfile.length % sizeof(GymLogEntry) != 0) {
  53. log("Log file corrupted.\n");
  54. } else {
  55. size_t entryCount = logfile.length / sizeof(GymLogEntry);
  56. result = { (GymLogEntry *)logfile.str, entryCount, entryCount };
  57. }
  58. return result;
  59. }
  60. struct WorkSummary {
  61. real32 totalWork;
  62. uint32 restTime;
  63. };
  64. WorkSummary workSummaryForExercise(list<GymLogEntry> entries) {
  65. WorkSummary result = {0};
  66. UnixTimestamp lastTimestamp = 0;
  67. for (EachInReversed(entries, i)) {
  68. GymLogEntry logEntry = entries.data[i];
  69. result.totalWork += logEntry.weightRepsInfo.weight * logEntry.weightRepsInfo.reps;
  70. if (lastTimestamp > 0) {
  71. result.restTime += (uint32)(lastTimestamp - logEntry.timestamp);
  72. }
  73. lastTimestamp = logEntry.timestamp;
  74. }
  75. return result;
  76. }
  77. int gymTrackerWorkToday(Arena *arena, uint32 exerciseId, string exerciseName) {
  78. int statusCode = 0;
  79. string logfile = os_readEntireFile(arena, LOG_FILE_LOCATION);
  80. if (logfile.length % sizeof(GymLogEntry) != 0) {
  81. log("Log file corrupted.\n");
  82. statusCode = 1;
  83. } else {
  84. size_t entryCount = logfile.length / sizeof(GymLogEntry);
  85. list<GymLogEntry> logEntries = { (GymLogEntry *)logfile.str, entryCount, entryCount };
  86. UnixTimestamp todayUnix = getSystemUnixTime();
  87. Timestamp todayTs = timestampFromUnixTime(&todayUnix);
  88. list<GymLogEntry> todaysEntries = {0};
  89. todaysEntries.data = logEntries.data;
  90. for (EachInReversed(logEntries, i)) {
  91. GymLogEntry logEntry = logEntries.data[i];
  92. Timestamp logTs = timestampFromUnixTime(&logEntry.timestamp);
  93. if (logTs.tm_yday == todayTs.tm_yday && todayTs.tm_year == logTs.tm_year) {
  94. todaysEntries.head += 1;
  95. todaysEntries.data = &logEntries.data[i];
  96. }
  97. }
  98. if (todaysEntries.data) {
  99. todaysEntries.length = todaysEntries.head;
  100. WorkSummary summary = workSummaryForExercise(todaysEntries);
  101. log("Total work today for %S:\n%.2fkg in ~%.2fmin.\n", exerciseName, summary.totalWork, (real32)summary.restTime / 60.0f);
  102. }
  103. }
  104. return statusCode;
  105. }
  106. int gymTrackerStatus(Arena *arena, list<string> args) {
  107. int statusCode = 0;
  108. string file = os_readEntireFile(arena, LOG_FILE_LOCATION);
  109. if (file.length % sizeof(GymLogEntry) != 0) {
  110. puts("Log file corrupted.");
  111. statusCode = 1;
  112. } else {
  113. GymLogDbParsed *db = parseDb(arena, os_readEntireFile(arena, DB_FILE_LOCATION));
  114. Timestamp startTs = {0};
  115. int numDays = 1;
  116. bool showAll = args.length == 1 && strEql(args.data[0], "--all"_s);
  117. if (!showAll) {
  118. if (args.length == 2 && (strEql(args.data[0], "--days"_s) || strEql(args.data[0], "-d"_s))) {
  119. size_t l;
  120. numDays = parsePositiveInt(args.data[1], &l);
  121. }
  122. if (numDays == -1) {
  123. puts("Bad argument for --days (-d) parameter.");
  124. statusCode = 1;
  125. } else {
  126. uint64 todayUnix = getSystemUnixTime();
  127. UnixTimestamp startUnix = todayUnix - numDays * 24 * 60 * 60;
  128. startTs = timestampFromUnixTime(&startUnix);
  129. }
  130. }
  131. if (statusCode == 0) {
  132. int lastDay = -1;
  133. int lastYear = -1;
  134. size_t entryCount = file.length / sizeof(GymLogEntry);
  135. list<GymLogEntry> logEntries = { (GymLogEntry *)file.str, entryCount, entryCount };
  136. list<string> nameByExercise = PushFullListZero(arena, string, db->header.nextId);
  137. list<real32> workPerExerciseByDay = PushFullListZero(arena, real32, db->header.nextId);
  138. list<uint32> restPerExerciseByDay = PushFullListZero(arena, uint32, db->header.nextId);
  139. list<uint32> lastTsPerExerciseByDay = PushFullListZero(arena, uint32, db->header.nextId);
  140. int dayCount = 0;
  141. Timestamp timestamp = {0};
  142. GymLogEntry *prevEntry = 0;
  143. GymLogEntry *entry = 0;
  144. for (EachIn(logEntries, i)) {
  145. prevEntry = entry;
  146. entry = &logEntries.data[i];
  147. timestamp = timestampFromUnixTime(&entry->timestamp);
  148. if (timestamp.tm_year < startTs.tm_year || timestamp.tm_yday < startTs.tm_yday) {
  149. continue;
  150. }
  151. if (timestamp.tm_yday != lastDay || timestamp.tm_year != lastYear) {
  152. if (dayCount > 0) {
  153. log("\n");
  154. }
  155. log("================ %S =================================\n", formatTimeYmd(arena, &timestamp));
  156. lastDay = timestamp.tm_yday;
  157. lastYear = timestamp.tm_year;
  158. dayCount++;
  159. }
  160. workPerExerciseByDay.data[entry->exerciseId] += entry->weightRepsInfo.reps * entry->weightRepsInfo.weight;
  161. uint32 lastTsForExercise = lastTsPerExerciseByDay.data[entry->exerciseId];
  162. if (lastTsForExercise > 0) {
  163. restPerExerciseByDay.data[entry->exerciseId] += (uint32)(entry->timestamp - lastTsForExercise);
  164. }
  165. lastTsPerExerciseByDay.data[entry->exerciseId] = (uint32)entry->timestamp;
  166. const char *format;
  167. if (entry->weightRepsInfo.weight == (int32)entry->weightRepsInfo.weight) {
  168. format = "%S: %S %.0fkg X %i\n";
  169. } else {
  170. format = "%S: %S %.2fkg X %i\n";
  171. }
  172. string *exerciseName = &(nameByExercise.data[entry->exerciseId]);
  173. if (exerciseName->str == 0) {
  174. for (EachIn(db->entries, j)) {
  175. GymLogDbParsedEntry dbEntry = db->entries.data[j];
  176. if (dbEntry.id == entry->exerciseId) {
  177. *exerciseName = dbEntry.name;
  178. }
  179. }
  180. }
  181. string nameToPrint = {0};
  182. if (prevEntry && entry->exerciseId == prevEntry->exerciseId) {
  183. nameToPrint = PushStringFill(arena, exerciseName->length, '.');
  184. } else {
  185. nameToPrint = exerciseName->str ? *exerciseName : "unknown-exercise"_s;
  186. log("\n");
  187. }
  188. log(format,
  189. formatTimeHms(arena, &timestamp),
  190. nameToPrint,
  191. entry->weightRepsInfo.weight,
  192. entry->weightRepsInfo.reps);
  193. Timestamp nextTimestamp = {0};
  194. if (i < logEntries.head - 1) {
  195. nextTimestamp = timestampFromUnixTime(&logEntries.data[i + 1].timestamp);
  196. }
  197. if (i == logEntries.head + 1 || nextTimestamp.tm_yday != lastDay || nextTimestamp.tm_year != lastYear) {
  198. log("\n");
  199. log("Work summary:\n");
  200. for (size_t j = 0; j < workPerExerciseByDay.length; j++) {
  201. if (workPerExerciseByDay.data[j] != 0.0f) {
  202. log("%S: %.2fkg in %.2fmin\n", nameByExercise.data[j], workPerExerciseByDay.data[j], (real32)restPerExerciseByDay.data[j] / 60.0f);
  203. }
  204. }
  205. zeroListFull(&workPerExerciseByDay);
  206. zeroListFull(&restPerExerciseByDay);
  207. zeroListFull(&lastTsPerExerciseByDay);
  208. }
  209. }
  210. }
  211. }
  212. return statusCode;
  213. }
  214. int gymTrackerDeleteEntries(Arena *arena, list<string> args) {
  215. int statusCode = 0;
  216. if (args.length == 0) {
  217. log("Please pass the number of entries to delete starting from the most recent.");
  218. statusCode = 1;
  219. } else {
  220. size_t position = 0;
  221. int numToDelete = parsePositiveInt(args.data[0], &position);
  222. if (numToDelete != -1) {
  223. list<GymLogEntry> logEntries = loadEntryLog(arena, LOG_FILE_LOCATION);
  224. if (numToDelete > logEntries.length) {
  225. log("%i is more than the current number of log entries (%i). Aborting.", numToDelete, logEntries.length);
  226. statusCode = 1;
  227. } else {
  228. os_writeEntireFile(arena, LOG_FILE_LOCATION, (byte *)logEntries.data, (logEntries.length - numToDelete) * sizeof(GymLogEntry));
  229. }
  230. } else {
  231. log("Invalid number to delete.\n");
  232. statusCode = 0;
  233. }
  234. }
  235. return statusCode;
  236. }
  237. // Syntax: do <exercise-name> weightKg reps
  238. int gymTrackerDo(Arena *arena, list<string> args) {
  239. int statusCode = 0;
  240. string exerciseName = {0};
  241. if (args.length < 3 || args.data[0].length == 0) {
  242. log("Invalid exercise name and/or number of arguments.\n");
  243. statusCode = 1;
  244. } else {
  245. exerciseName = args.data[0];
  246. }
  247. GymLogDbParsedEntry *existingEntry = 0;
  248. if (statusCode == 0) {
  249. GymLogDbParsed *db = parseDb(arena, os_readEntireFile(arena, DB_FILE_LOCATION));
  250. for (EachIn(db->entries, i)) {
  251. GymLogDbParsedEntry entry = db->entries.data[i];
  252. if (strStartsWith(entry.name, exerciseName)) {
  253. existingEntry = &entry;
  254. if (entry.name.length != exerciseName.length) {
  255. exerciseName = entry.name;
  256. log("Assuming exercise \"%S\".\n\n", entry.name);
  257. }
  258. break;
  259. }
  260. }
  261. if (!existingEntry) {
  262. log("The exercise \"%S\" hasn't been registered.", exerciseName);
  263. statusCode = 1;
  264. }
  265. }
  266. if (statusCode == 0) {
  267. uint32 exerciseId = existingEntry->id;
  268. size_t parsedCount = 0;
  269. real32 kg = parsePositiveReal32(args.data[1], &parsedCount);
  270. uint8 reps = parsePositiveInt(args.data[2], &parsedCount);
  271. if (parsedCount == 0 || kg == NAN || reps == 0 || kg == 0) {
  272. log("Invalid reps or weight input.\n");
  273. statusCode = 1;
  274. } else {
  275. GymLogEntry entry = {
  276. getSystemUnixTime(),
  277. exerciseId,
  278. reps,
  279. kg,
  280. };
  281. os_fileAppend(arena, LOG_FILE_LOCATION, (byte *)&entry, sizeof(entry));
  282. statusCode = gymTrackerWorkToday(arena, exerciseId, exerciseName);
  283. }
  284. }
  285. return statusCode;
  286. }
  287. int gymTrackerListExercises(Arena *arena, list<string> args) {
  288. int statusCode = 0;
  289. GymLogDbParsed *db = parseDb(arena, os_readEntireFile(arena, DB_FILE_LOCATION));
  290. if (db->entries.length == 0) {
  291. log("No entries currently registered in the exercise database.");
  292. } else {
  293. log("%i entries currently registered in the exercise database:\n\n", db->entries.length);
  294. for (EachIn(db->entries, i)) {
  295. log("#%i: %S\n", i + 1, db->entries.data[i].name);
  296. }
  297. }
  298. return statusCode;
  299. }
  300. int gymTrackerAddExercise(Arena *arena, list<string> args) {
  301. int statusCode = 0;
  302. string newExerciseName = args.data[0];
  303. if (newExerciseName.length == 0) {
  304. log("No exercise name provided.\n");
  305. statusCode = 1;
  306. }
  307. if (statusCode != 1) {
  308. string database = os_readEntireFile(arena, DB_FILE_LOCATION);
  309. byte *buf = 0;
  310. size_t newEntryStartIndex = 0;
  311. if (database.length == 0) {
  312. // Initialise DB
  313. newEntryStartIndex = sizeof(GymLogDbHeader);
  314. buf = PushArray(arena, byte, sizeof(GymLogDbHeader) + sizeof(GymLogDbEntry) + newExerciseName.length);
  315. GymLogDbHeader *header = (GymLogDbHeader *)buf;
  316. header->nextId = 1;
  317. } else {
  318. // Validate entry not already present
  319. bool invalid = false;
  320. GymLogDbHeader *header = (GymLogDbHeader *)database.str;
  321. size_t head = sizeof(GymLogDbHeader);
  322. uint32 entriesLeft = header->nextId - 1;
  323. while (entriesLeft > 0 && head < database.length) {
  324. GymLogDbEntry *currentEntry = (GymLogDbEntry *)((byte *)database.str + head);
  325. head += sizeof(GymLogDbEntry);
  326. string entryName = {(char *)((byte *)database.str + head), currentEntry->nameLength };
  327. if (strEql(entryName, newExerciseName)) {
  328. invalid = true;
  329. log("Exercise \"%S\" already registered (entry #%i)\n", entryName, currentEntry->id);
  330. break;
  331. }
  332. head += currentEntry->nameLength;
  333. }
  334. if (!invalid) {
  335. newEntryStartIndex = database.length;
  336. buf = PushArray(arena, byte, database.length + sizeof(GymLogDbEntry) + newExerciseName.length);
  337. memcpy(buf, database.str, database.length);
  338. } else {
  339. statusCode = 1;
  340. }
  341. }
  342. if (statusCode != 1) {
  343. // Add entry
  344. GymLogDbHeader *header = (GymLogDbHeader *)buf;
  345. GymLogDbEntry *entry = (GymLogDbEntry *)(buf + newEntryStartIndex);
  346. entry->id = header->nextId;
  347. entry->nameLength = (uint32)newExerciseName.length;
  348. header->nextId++;
  349. byte *newExerciseNameDb = buf + newEntryStartIndex + sizeof(GymLogDbEntry);
  350. memcpy(newExerciseNameDb, newExerciseName.str, newExerciseName.length);
  351. size_t bufSize = newEntryStartIndex + sizeof(GymLogDbEntry) + newExerciseName.length;
  352. os_writeEntireFile(arena, DB_FILE_LOCATION, buf, bufSize);
  353. }
  354. }
  355. return statusCode;
  356. }
  357. int main(int argc, char **argv) {
  358. initialiseCore();
  359. Arena *arena = arenaAlloc(Megabytes(64));
  360. list<string> args = getArgs(arena, argc, argv);
  361. int statusCode = 0;
  362. if (args.length < 1) {
  363. log("At least one arg is required.\n");
  364. statusCode = 1;
  365. }
  366. if (statusCode == 0) {
  367. string cmd = args.data[0];
  368. list<string> argsRest = listSlice(args, 1);
  369. if (strEql("status"_s, cmd)) {
  370. statusCode = gymTrackerStatus(arena, argsRest);
  371. } else if (strEql("do"_s, cmd)) {
  372. statusCode = gymTrackerDo(arena, argsRest);
  373. } else if (strEql("delete"_s, cmd)) {
  374. statusCode = gymTrackerDeleteEntries(arena, argsRest);
  375. } else if (strEql("list"_s, cmd)) {
  376. statusCode = gymTrackerListExercises(arena, argsRest);
  377. } else if (strEql("add"_s, cmd)) {
  378. statusCode = gymTrackerAddExercise(arena, argsRest);
  379. } else {
  380. log("Unknown command \"%S\"\n", args.data[0]);
  381. statusCode = 1;
  382. }
  383. }
  384. return statusCode;
  385. }