25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

454 lines
16 KiB

  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. }