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

app.cpp 12 KiB

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週間前
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週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
1週間前
2週間前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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. }