No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 

341 líneas
12 KiB

  1. #include <math.h>
  2. #include <time.h>
  3. #include "core.hpp"
  4. const string LOG_FILE_LOCATION = strlit(".\\log.gtl");
  5. const string DB_FILE_LOCATION = strlit(".\\db.gtd");
  6. struct GymLogDbHeader {
  7. uint32 nextId;
  8. };
  9. struct GymLogDbEntry {
  10. uint32 id;
  11. uint32 nameLength;
  12. };
  13. struct GymLogDbParsedEntry {
  14. uint32 id;
  15. string name;
  16. };
  17. struct GymLogDbParsed {
  18. GymLogDbHeader header;
  19. list<GymLogDbParsedEntry> entries;
  20. };
  21. struct GymLogEntry {
  22. uint64 timestamp;
  23. uint32 exerciseId;
  24. union {
  25. struct WeightReps {
  26. uint8 reps;
  27. real32 weight;
  28. } weightRepsInfo;
  29. };
  30. };
  31. GymLogDbParsed *parseDb(Arena *arena, string database) {
  32. GymLogDbParsed *dbParsed = PushStruct(arena, GymLogDbParsed);
  33. GymLogDbHeader *header = (GymLogDbHeader *)database.str;
  34. dbParsed->header = *header;
  35. size_t head = sizeof(GymLogDbHeader);
  36. uint32 entriesLeft = header->nextId - 1;
  37. dbParsed->entries = PushList(arena, GymLogDbParsedEntry, entriesLeft);
  38. while (entriesLeft > 0 && head < database.length) {
  39. GymLogDbEntry *currentEntry = (GymLogDbEntry *)((byte *)database.str + head);
  40. GymLogDbParsedEntry parsedEntry = { currentEntry->id, PushString(arena, currentEntry->nameLength) };
  41. head += sizeof(GymLogDbEntry);
  42. memcpy(parsedEntry.name.str, database.str + head, currentEntry->nameLength);
  43. appendList(&dbParsed->entries, parsedEntry);
  44. head += currentEntry->nameLength;
  45. }
  46. return dbParsed;
  47. }
  48. int gymTrackerWorkForExercise(Arena *arena, uint32 exerciseId) {
  49. int statusCode = 0;
  50. string logfile = readEntireFile(arena, LOG_FILE_LOCATION);
  51. if (logfile.length % sizeof(GymLogEntry) != 0) {
  52. puts("Log file corrupted.");
  53. statusCode = 1;
  54. } else {
  55. size_t entryCount = logfile.length / sizeof(GymLogEntry);
  56. int currentDay = 0;
  57. int currentYear = 0;
  58. list<GymLogEntry> logEntries = { (GymLogEntry *)logfile.str, entryCount, entryCount };
  59. tm todayTs = {0};
  60. time_t todayUnix = getSystemUnixTime();
  61. gmtime_s(&todayTs, (time_t *)&todayUnix);
  62. real32 work = 0;
  63. for (EachIn(logEntries, i)) {
  64. GymLogEntry logEntry = logEntries.data[i];
  65. tm logTs = {0};
  66. gmtime_s(&logTs, (time_t *)&logEntry.timestamp);
  67. if (logTs.tm_yday == todayTs.tm_yday && todayTs.tm_year == logTs.tm_year) {
  68. work += logEntry.weightRepsInfo.weight * logEntry.weightRepsInfo.reps;
  69. }
  70. }
  71. printf("Total work for this exercise today:\n%.2f J * m/ex\n", work);
  72. }
  73. return statusCode;
  74. }
  75. int gymTrackerStatus(Arena *arena, list<string> args) {
  76. int statusCode = 0;
  77. string file = readEntireFile(arena, LOG_FILE_LOCATION);
  78. GymLogDbParsed *db = parseDb(arena, readEntireFile(arena, DB_FILE_LOCATION));
  79. if (file.length % sizeof(GymLogEntry) != 0) {
  80. puts("Log file corrupted.");
  81. statusCode = 1;
  82. } else {
  83. tm stopTs = {0};
  84. if (args.length > 0 && strEql(args.data[0], strlit("--today"))) {
  85. time_t todayUnix = getSystemUnixTime();
  86. gmtime_s(&stopTs, (time_t *)&todayUnix);
  87. } else if (args.length > 1 && strEql(args.data[0], strlit("--days")) || strEql(args.data[0], strlit("-d"))) {
  88. size_t l;
  89. int numDays = parsePositiveInt(args.data[1], &l);
  90. if (numDays == -1) {
  91. puts("Bad argument for --days parameter.");
  92. statusCode = 1;
  93. } else {
  94. uint64 todayUnix = getSystemUnixTime();
  95. time_t stopUnix = (time_t)(todayUnix - numDays * 24 * 60 * 60);
  96. gmtime_s(&stopTs, (time_t *)&stopUnix);
  97. }
  98. }
  99. if (statusCode == 0) {
  100. size_t entryCount = file.length / sizeof(GymLogEntry);
  101. int currentDay = -1;
  102. int currentYear = -1;
  103. list<GymLogEntry> logEntries = { (GymLogEntry *)file.str, entryCount, entryCount };
  104. list<real32> workPerExerciseByDay = PushFullList(arena, real32, db->header.nextId);
  105. list<string> nameByExercise = PushFullList(arena, string, db->header.nextId);
  106. zeroListFull(&workPerExerciseByDay);
  107. zeroListFull(&nameByExercise);
  108. int dayCount = 0;
  109. tm timestamp = {0};
  110. if (logEntries.length > 0) {
  111. gmtime_s(&timestamp, (time_t *)&logEntries.data[logEntries.length - 1].timestamp);
  112. }
  113. for (EachInReversed(logEntries, i)) {
  114. GymLogEntry entry = logEntries.data[i];
  115. if (timestamp.tm_yday != currentDay || timestamp.tm_year != currentYear) {
  116. if (timestamp.tm_year < stopTs.tm_year || timestamp.tm_yday < stopTs.tm_yday) {
  117. break;
  118. }
  119. if (dayCount > 0) {
  120. puts("");
  121. }
  122. printf("--- %s ---\n", cstring(arena, formatTimeYmd(arena, &timestamp)));
  123. currentDay = timestamp.tm_yday;
  124. currentYear = timestamp.tm_year;
  125. dayCount++;
  126. }
  127. workPerExerciseByDay.data[entry.exerciseId] += entry.weightRepsInfo.reps * entry.weightRepsInfo.weight;
  128. char *format;
  129. if (entry.weightRepsInfo.weight == (int32)entry.weightRepsInfo.weight) {
  130. format = "%s: %s, %.0fkg X %i\n";
  131. } else {
  132. format = "%s: %s, %.2fkg X %i\n";
  133. }
  134. string *exerciseName = &(nameByExercise.data[entry.exerciseId]);
  135. if (exerciseName->str == 0) {
  136. for (EachIn(db->entries, j)) {
  137. GymLogDbParsedEntry dbEntry = db->entries.data[j];
  138. if (dbEntry.id == entry.exerciseId) {
  139. *exerciseName = dbEntry.name;
  140. }
  141. }
  142. }
  143. printf(format,
  144. cstring(arena, formatTimeHms(arena, &timestamp)),
  145. exerciseName->str ? cstring(arena, *exerciseName) : "unknown-exercise",
  146. entry.weightRepsInfo.weight,
  147. entry.weightRepsInfo.reps);
  148. if (i > 0) {
  149. gmtime_s(&timestamp, (time_t *)&logEntries.data[i - 1].timestamp);
  150. }
  151. if (i == 0 || timestamp.tm_yday != currentDay || timestamp.tm_year != currentYear) {
  152. puts("");
  153. puts("Work summary:");
  154. for (size_t j = 0; j < workPerExerciseByDay.length; j++) {
  155. if (workPerExerciseByDay.data[j] != 0.0f) {
  156. printf("%s: %.2f J * m/ex\n", cstring(arena, nameByExercise.data[j]), workPerExerciseByDay.data[j]);
  157. }
  158. }
  159. zeroListFull(&workPerExerciseByDay);
  160. }
  161. }
  162. }
  163. }
  164. return statusCode;
  165. }
  166. // Syntax: do <exercise-name> weightKg reps
  167. int gymTrackerDo(Arena *arena, list<string> args) {
  168. int statusCode = 0;
  169. string newExerciseName = {0};
  170. if (args.length < 3 || args.data[0].length == 0) {
  171. printf("Invalid exercise name and/or number of arguments.");
  172. statusCode = 1;
  173. } else {
  174. newExerciseName = args.data[0];
  175. }
  176. GymLogDbParsedEntry *existingEntry = 0;
  177. if (statusCode == 0) {
  178. GymLogDbParsed *db = parseDb(arena, readEntireFile(arena, DB_FILE_LOCATION));
  179. for (EachIn(db->entries, i)) {
  180. GymLogDbParsedEntry entry = db->entries.data[i];
  181. if (strEql(entry.name, newExerciseName)) {
  182. existingEntry = &entry;
  183. break;
  184. }
  185. }
  186. if (!existingEntry) {
  187. statusCode = 1;
  188. }
  189. }
  190. if (statusCode == 0) {
  191. uint32 exerciseId = existingEntry->id;
  192. size_t parsedCount = 0;
  193. real32 kg = parsePositiveReal32(arena, args.data[1], &parsedCount);
  194. uint8 reps = parsePositiveInt(args.data[2], &parsedCount);
  195. if (parsedCount == 0 || kg == NAN || reps == 0 || kg == 0) {
  196. printf("Invalid reps or weight input.");
  197. statusCode = 1;
  198. } else {
  199. GymLogEntry entry = {
  200. getSystemUnixTime(),
  201. exerciseId,
  202. reps,
  203. kg,
  204. };
  205. fileAppend(arena, LOG_FILE_LOCATION, (byte *)&entry, sizeof(entry));
  206. statusCode = gymTrackerWorkForExercise(arena, exerciseId);
  207. }
  208. }
  209. return statusCode;
  210. }
  211. int gymTrackerAddExercise(Arena *arena, list<string> args) {
  212. int statusCode = 0;
  213. string newExerciseName = args.data[0];
  214. if (newExerciseName.length == 0) {
  215. printf("No exercise name provided.");
  216. statusCode = 1;
  217. }
  218. if (statusCode != 1) {
  219. string databaseLocation = DB_FILE_LOCATION;
  220. string database = readEntireFile(arena, databaseLocation);
  221. byte *buf = 0;
  222. size_t newEntryStartIndex = 0;
  223. if (database.length == 0) {
  224. // Initialise DB
  225. newEntryStartIndex = sizeof(GymLogDbHeader);
  226. buf = PushArray(arena, byte, sizeof(GymLogDbHeader) + sizeof(GymLogDbEntry) + newExerciseName.length);
  227. GymLogDbHeader *header = (GymLogDbHeader *)buf;
  228. header->nextId = 1;
  229. } else {
  230. // Validate entry not already present
  231. bool invalid = false;
  232. GymLogDbHeader *header = (GymLogDbHeader *)database.str;
  233. size_t head = sizeof(GymLogDbHeader);
  234. uint32 entriesLeft = header->nextId - 1;
  235. while (entriesLeft > 0 && head < database.length) {
  236. GymLogDbEntry *currentEntry = (GymLogDbEntry *)((byte *)database.str + head);
  237. head += sizeof(GymLogDbEntry);
  238. string entryName = {(char *)((byte *)database.str + head), currentEntry->nameLength };
  239. if (strEql(entryName, newExerciseName)) {
  240. invalid = true;
  241. printf("Exercise \"%s\" already registered (entry #%i)\n", cstring(arena, entryName), currentEntry->id);
  242. break;
  243. }
  244. head += currentEntry->nameLength;
  245. }
  246. if (!invalid) {
  247. newEntryStartIndex = database.length;
  248. buf = PushArray(arena, byte, database.length + sizeof(GymLogDbEntry) + newExerciseName.length);
  249. memcpy(buf, database.str, database.length);
  250. } else {
  251. statusCode = 1;
  252. }
  253. }
  254. if (statusCode != 1) {
  255. // Add entry
  256. GymLogDbHeader *header = (GymLogDbHeader *)buf;
  257. GymLogDbEntry *entry = (GymLogDbEntry *)(buf + newEntryStartIndex);
  258. entry->id = header->nextId;
  259. entry->nameLength = (uint32)newExerciseName.length;
  260. header->nextId++;
  261. byte *newExerciseNameDb = buf + newEntryStartIndex + sizeof(GymLogDbEntry);
  262. memcpy(newExerciseNameDb, newExerciseName.str, newExerciseName.length);
  263. size_t bufSize = newEntryStartIndex + sizeof(GymLogDbEntry) + newExerciseName.length;
  264. writeEntireFile(arena, databaseLocation, buf, bufSize);
  265. }
  266. }
  267. return statusCode;
  268. }
  269. int main(int argc, char **argv) {
  270. Arena *arena = arenaAlloc(Megabytes(64));
  271. list<string> args = getArgs(arena, argc, argv);
  272. int statusCode = 0;
  273. if (args.length < 1) {
  274. puts("At least one arg is required.");
  275. statusCode = 1;
  276. }
  277. if (statusCode == 0) {
  278. if (strEql(args.data[0], strlit("status"))) {
  279. statusCode = gymTrackerStatus(arena, listSlice(args, 1));
  280. } else if (strEql(args.data[0], strlit("do"))) {
  281. statusCode = gymTrackerDo(arena, listSlice(args, 1));
  282. } else if (strEql(args.data[0], strlit("add"))) {
  283. statusCode = gymTrackerAddExercise(arena, listSlice(args, 1));
  284. } else {
  285. printf("Unknown command \"%s\"", cstring(arena, args.data[0]));
  286. statusCode = 1;
  287. }
  288. }
  289. return statusCode;
  290. }