Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

342 linhas
12 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. GymLogDbHeader *header = (GymLogDbHeader *)database.str;
  36. dbParsed->header = *header;
  37. size_t head = sizeof(GymLogDbHeader);
  38. uint32 entriesLeft = header->nextId - 1;
  39. dbParsed->entries = PushList(arena, GymLogDbParsedEntry, entriesLeft);
  40. while (entriesLeft > 0 && head < database.length) {
  41. GymLogDbEntry *currentEntry = (GymLogDbEntry *)((byte *)database.str + head);
  42. GymLogDbParsedEntry parsedEntry = { currentEntry->id, PushString(arena, currentEntry->nameLength) };
  43. head += sizeof(GymLogDbEntry);
  44. memcpy(parsedEntry.name.str, database.str + head, currentEntry->nameLength);
  45. appendList(&dbParsed->entries, parsedEntry);
  46. head += currentEntry->nameLength;
  47. }
  48. return dbParsed;
  49. }
  50. int gymTrackerWorkForExercise(Arena *arena, uint32 exerciseId) {
  51. int statusCode = 0;
  52. string logfile = readEntireFile(arena, LOG_FILE_LOCATION);
  53. if (logfile.length % sizeof(GymLogEntry) != 0) {
  54. log("Log file corrupted.\n");
  55. statusCode = 1;
  56. } else {
  57. size_t entryCount = logfile.length / sizeof(GymLogEntry);
  58. int currentDay = 0;
  59. int currentYear = 0;
  60. list<GymLogEntry> logEntries = { (GymLogEntry *)logfile.str, entryCount, entryCount };
  61. UnixTimestamp todayUnix = getSystemUnixTime();
  62. Timestamp todayTs = timestampFromUnixTime(&todayUnix);
  63. real32 work = 0;
  64. for (EachIn(logEntries, i)) {
  65. GymLogEntry logEntry = logEntries.data[i];
  66. Timestamp logTs = timestampFromUnixTime(&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. log("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. Timestamp stopTs = {0};
  84. if (args.length > 0 && strEql(args.data[0], "--today"_s)) {
  85. UnixTimestamp nowUnix = getSystemUnixTime();
  86. stopTs = timestampFromUnixTime(&nowUnix);
  87. } else if (args.length > 1 && strEql(args.data[0], "--days"_s) || strEql(args.data[0], "-d"_s)) {
  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. UnixTimestamp stopUnix = todayUnix - numDays * 24 * 60 * 60;
  96. stopTs = timestampFromUnixTime(&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. Timestamp timestamp = {0};
  110. if (logEntries.length > 0) {
  111. timestamp = timestampFromUnixTime(&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. log("\n");
  121. }
  122. log("--- %S ---\n", 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. const 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. log(format,
  144. formatTimeHms(arena, &timestamp),
  145. exerciseName->str ? *exerciseName : "unknown-exercise"_s,
  146. entry.weightRepsInfo.weight,
  147. entry.weightRepsInfo.reps);
  148. if (i > 0) {
  149. timestamp = timestampFromUnixTime(&logEntries.data[i - 1].timestamp);
  150. }
  151. if (i == 0 || timestamp.tm_yday != currentDay || timestamp.tm_year != currentYear) {
  152. log("\n");
  153. log("Work summary:\n");
  154. for (size_t j = 0; j < workPerExerciseByDay.length; j++) {
  155. if (workPerExerciseByDay.data[j] != 0.0f) {
  156. log("%S: %.2f J * m/ex\n", 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. log("Invalid exercise name and/or number of arguments.\n");
  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(args.data[1], &parsedCount);
  194. uint8 reps = parsePositiveInt(args.data[2], &parsedCount);
  195. if (parsedCount == 0 || kg == NAN || reps == 0 || kg == 0) {
  196. log("Invalid reps or weight input.\n");
  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. log("No exercise name provided.\n");
  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. log("Exercise \"%S\" already registered (entry #%i)\n", 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. initialiseCore();
  271. Arena *arena = arenaAlloc(Megabytes(64));
  272. list<string> args = getArgs(arena, argc, argv);
  273. int statusCode = 0;
  274. if (args.length < 1) {
  275. log("At least one arg is required.\n");
  276. statusCode = 1;
  277. }
  278. if (statusCode == 0) {
  279. if (strEql(args.data[0], "status"_s)) {
  280. statusCode = gymTrackerStatus(arena, listSlice(args, 1));
  281. } else if (strEql(args.data[0], "do"_s)) {
  282. statusCode = gymTrackerDo(arena, listSlice(args, 1));
  283. } else if (strEql(args.data[0], "add"_s)) {
  284. statusCode = gymTrackerAddExercise(arena, listSlice(args, 1));
  285. } else {
  286. log("Unknown command \"%S\"\n", args.data[0]);
  287. statusCode = 1;
  288. }
  289. }
  290. return statusCode;
  291. }