@swimedge/metrics 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -88,6 +88,12 @@ export declare enum MetricCategory {
88
88
  TECHNIQUE = "technique",
89
89
  ENERGY = "energy"
90
90
  }
91
+ export declare const formatNumber: (value: number, decimals?: number) => string;
92
+ export declare const formatPace: (pace: string | number) => string;
93
+ export declare const formatTime: (seconds: number) => string;
94
+ export declare const formatHeartRate: (bpm: number) => string;
95
+ export declare const formatDistance: (meters: number) => string;
96
+ export declare const formatAverageTurnTime: (session: SwimSession) => string;
91
97
  export interface MetricDefinition {
92
98
  key: string;
93
99
  label: string;
@@ -100,10 +106,15 @@ export interface MetricDefinition {
100
106
  formula?: string;
101
107
  source?: string;
102
108
  healthKitSource?: string;
109
+ isHealthKit?: boolean;
110
+ decimals?: number;
103
111
  availableFrom: {
104
112
  watch: boolean;
105
113
  manual: boolean;
106
114
  };
115
+ formatter?: (session: SwimSession) => string | number | null;
116
+ lapFormatter?: (value: any) => string;
117
+ condition?: (session: SwimSession) => boolean;
107
118
  }
108
119
  export declare const METRICS_REGISTRY: Record<keyof SwimSession, MetricDefinition>;
109
120
  export declare const MetricsHelper: {
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Shared metrics registry for frontend, backend, and watch app
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.MetricsHelper = exports.METRICS_REGISTRY = exports.MetricCategory = exports.SwimType = void 0;
7
+ exports.MetricsHelper = exports.METRICS_REGISTRY = exports.formatAverageTurnTime = exports.formatDistance = exports.formatHeartRate = exports.formatTime = exports.formatPace = exports.formatNumber = exports.MetricCategory = exports.SwimType = void 0;
8
8
  var SwimType;
9
9
  (function (SwimType) {
10
10
  SwimType["INDOOR"] = "indoor";
@@ -17,30 +17,73 @@ var MetricCategory;
17
17
  MetricCategory["TECHNIQUE"] = "technique";
18
18
  MetricCategory["ENERGY"] = "energy";
19
19
  })(MetricCategory || (exports.MetricCategory = MetricCategory = {}));
20
+ // Formatting helpers
21
+ const formatNumber = (value, decimals = 2) => {
22
+ return Number(value).toFixed(decimals);
23
+ };
24
+ exports.formatNumber = formatNumber;
25
+ const formatPace = (pace) => {
26
+ if (typeof pace === 'string')
27
+ return pace;
28
+ const minutes = Math.floor(pace / 60);
29
+ const seconds = Math.round(pace % 60);
30
+ return `${minutes}:${String(seconds).padStart(2, '0')}`;
31
+ };
32
+ exports.formatPace = formatPace;
33
+ const formatTime = (seconds) => {
34
+ const mins = Math.floor(seconds / 60);
35
+ const secs = Math.floor(seconds % 60);
36
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
37
+ };
38
+ exports.formatTime = formatTime;
39
+ const formatHeartRate = (bpm) => {
40
+ return `${Math.round(bpm)} bpm`;
41
+ };
42
+ exports.formatHeartRate = formatHeartRate;
43
+ const formatDistance = (meters) => {
44
+ return meters >= 1000
45
+ ? `${(meters / 1000).toFixed(1)}km`
46
+ : `${Math.round(meters)}m`;
47
+ };
48
+ exports.formatDistance = formatDistance;
49
+ const formatAverageTurnTime = (session) => {
50
+ if (!session.lapTurnTimes?.length)
51
+ return '';
52
+ const validTurnTimes = session.lapTurnTimes.filter(t => !isNaN(t) && t > 0);
53
+ if (!validTurnTimes.length)
54
+ return '';
55
+ const avgTime = validTurnTimes.reduce((a, b) => a + b, 0) / validTurnTimes.length;
56
+ return `${(0, exports.formatNumber)(avgTime, 1)}s`;
57
+ };
58
+ exports.formatAverageTurnTime = formatAverageTurnTime;
20
59
  exports.METRICS_REGISTRY = {
21
60
  distance: {
22
61
  key: 'distance',
23
62
  label: 'Distance',
24
63
  unit: 'm',
25
64
  category: MetricCategory.PERFORMANCE,
26
- description: 'Total distance swum',
65
+ description: 'Total distance swum in this session. Measured in meters or kilometers.',
27
66
  icon: 'resize-outline',
28
67
  isPerSession: true,
29
68
  source: 'HealthKit: HKWorkout distance',
30
69
  healthKitSource: 'HKWorkout.totalDistance',
70
+ isHealthKit: true,
31
71
  availableFrom: { watch: true, manual: true },
72
+ formatter: (s) => (0, exports.formatDistance)(s.distance),
32
73
  },
33
74
  time: {
34
75
  key: 'time',
35
76
  label: 'Duration',
36
77
  unit: 's',
37
78
  category: MetricCategory.PERFORMANCE,
38
- description: 'Total workout duration',
79
+ description: 'Total time spent swimming, including active swimming and rest periods.',
39
80
  icon: 'time-outline',
40
81
  isPerSession: true,
41
82
  source: 'HealthKit: HKWorkout duration',
42
83
  healthKitSource: 'HKWorkout.duration',
84
+ isHealthKit: true,
43
85
  availableFrom: { watch: true, manual: true },
86
+ formatter: (s) => (0, exports.formatTime)(s.time),
44
87
  },
45
88
  activeTime: {
46
89
  key: 'activeTime',
@@ -50,7 +93,9 @@ exports.METRICS_REGISTRY = {
50
93
  description: 'Time actively swimming',
51
94
  icon: 'play-outline',
52
95
  isPerSession: true,
53
- source: 'Calculated from stroke detection with 5-second timeout',
96
+ source: 'HealthKit: HKWorkoutBuilder elapsedTime (excludes rest periods)',
97
+ healthKitSource: 'HKWorkoutBuilder.elapsedTime',
98
+ isHealthKit: true,
54
99
  availableFrom: { watch: true, manual: false },
55
100
  },
56
101
  restTime: {
@@ -58,202 +103,249 @@ exports.METRICS_REGISTRY = {
58
103
  label: 'Rest Time',
59
104
  unit: 's',
60
105
  category: MetricCategory.PERFORMANCE,
61
- description: 'Time resting',
106
+ description: 'Total time spent resting between swimming intervals.',
62
107
  icon: 'pause-outline',
63
108
  isPerSession: true,
64
109
  formula: 'Total Duration - Active Time',
65
110
  availableFrom: { watch: true, manual: false },
111
+ formatter: (s) => (0, exports.formatTime)(s.restTime),
112
+ condition: (s) => !!s.restTime,
66
113
  },
67
114
  laps: {
68
115
  key: 'laps',
69
116
  label: 'Laps',
70
117
  unit: '',
71
118
  category: MetricCategory.PERFORMANCE,
72
- description: 'Pool lengths',
119
+ description: 'Number of pool lengths completed during the swimming session.',
73
120
  icon: 'layers-outline',
74
121
  isPerSession: true,
75
122
  formula: 'Distance ÷ Pool Length',
76
123
  availableFrom: { watch: true, manual: true },
124
+ formatter: (s) => String(s.laps),
125
+ condition: (s) => !!s.laps,
77
126
  },
78
127
  avgPace: {
79
128
  key: 'avgPace',
80
129
  label: 'Pace',
81
130
  unit: '/100m',
82
131
  category: MetricCategory.PERFORMANCE,
83
- description: 'Average pace',
132
+ description: 'Average time per 100 meters. Lower values indicate faster swimming speed.',
84
133
  icon: 'speedometer-outline',
85
134
  isPerSession: true,
86
135
  formula: '(Active Time ÷ Distance) × 100',
87
136
  availableFrom: { watch: true, manual: true },
137
+ formatter: (s) => (0, exports.formatPace)(s.avgPace),
138
+ condition: (s) => !!s.avgPace,
88
139
  },
89
140
  poolLength: {
90
141
  key: 'poolLength',
91
142
  label: 'Pool Length',
92
143
  unit: 'm',
93
144
  category: MetricCategory.PERFORMANCE,
94
- description: 'Pool length',
145
+ description: 'Length of the swimming pool used for this session.',
95
146
  icon: 'resize-outline',
96
147
  isPerSession: true,
97
148
  source: 'User configuration setting',
98
149
  availableFrom: { watch: true, manual: true },
150
+ formatter: (s) => `${s.poolLength}m`,
151
+ condition: (s) => !!s.poolLength,
99
152
  },
100
153
  swimType: {
101
154
  key: 'swimType',
102
- label: 'Swim Type',
155
+ label: 'Swimming Type',
103
156
  unit: '',
104
157
  category: MetricCategory.PERFORMANCE,
105
- description: 'Indoor/Open Water',
158
+ description: 'Type of swimming activity (pool, open water, etc.).',
106
159
  icon: 'water-outline',
107
160
  isPerSession: true,
108
161
  source: 'User selection or workout location type',
109
162
  availableFrom: { watch: true, manual: true },
163
+ formatter: (s) => s.swimType,
164
+ condition: (s) => !!s.swimType,
110
165
  },
111
166
  waterTemp: {
112
167
  key: 'waterTemp',
113
168
  label: 'Water Temp',
114
169
  unit: '°C',
115
170
  category: MetricCategory.PERFORMANCE,
116
- description: 'Water temperature',
171
+ description: 'Temperature of the water during the swimming session.',
117
172
  icon: 'thermometer-outline',
118
173
  isPerSession: true,
119
174
  source: 'Apple Watch water temperature sensor',
175
+ isHealthKit: true,
120
176
  availableFrom: { watch: true, manual: false },
177
+ formatter: (s) => `${(0, exports.formatNumber)(s.waterTemp, 1)}°C`,
178
+ condition: (s) => !!s.waterTemp,
121
179
  },
122
180
  avgHeartRate: {
123
181
  key: 'avgHeartRate',
124
182
  label: 'Avg Heart Rate',
125
183
  unit: 'bpm',
126
184
  category: MetricCategory.HEALTH,
127
- description: 'Average heart rate',
185
+ description: 'Average heart rate throughout the swimming session.',
128
186
  icon: 'heart-outline',
129
187
  isPerSession: true,
130
188
  source: 'HealthKit: HKQuantityTypeIdentifierHeartRate',
131
189
  healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
190
+ isHealthKit: true,
132
191
  availableFrom: { watch: true, manual: false },
192
+ formatter: (s) => (0, exports.formatHeartRate)(s.avgHeartRate),
193
+ condition: (s) => !!s.avgHeartRate,
133
194
  },
134
195
  maxHeartRate: {
135
196
  key: 'maxHeartRate',
136
197
  label: 'Max Heart Rate',
137
198
  unit: 'bpm',
138
199
  category: MetricCategory.HEALTH,
139
- description: 'Maximum heart rate',
200
+ description: 'Highest heart rate recorded during the session.',
140
201
  icon: 'trending-up-outline',
141
202
  isPerSession: true,
142
203
  source: 'HealthKit: HKQuantityTypeIdentifierHeartRate',
143
204
  healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
205
+ isHealthKit: true,
144
206
  availableFrom: { watch: true, manual: false },
207
+ formatter: (s) => (0, exports.formatHeartRate)(s.maxHeartRate),
208
+ condition: (s) => !!s.maxHeartRate,
145
209
  },
146
210
  minHeartRate: {
147
211
  key: 'minHeartRate',
148
212
  label: 'Min Heart Rate',
149
213
  unit: 'bpm',
150
214
  category: MetricCategory.HEALTH,
151
- description: 'Minimum heart rate',
215
+ description: 'Lowest heart rate recorded during the session.',
152
216
  icon: 'trending-down-outline',
153
217
  isPerSession: true,
154
218
  source: 'HealthKit: HKQuantityTypeIdentifierHeartRate',
155
219
  healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
220
+ isHealthKit: true,
156
221
  availableFrom: { watch: true, manual: false },
222
+ formatter: (s) => (0, exports.formatHeartRate)(s.minHeartRate),
223
+ condition: (s) => !!s.minHeartRate,
157
224
  },
158
225
  calories: {
159
226
  key: 'calories',
160
227
  label: 'Calories',
161
228
  unit: 'kcal',
162
229
  category: MetricCategory.ENERGY,
163
- description: 'Total calories',
230
+ description: 'Total calories burned during the swimming session.',
164
231
  icon: 'flame-outline',
165
232
  isPerSession: true,
166
233
  source: 'HealthKit: HKQuantityTypeIdentifierActiveEnergyBurned',
167
234
  healthKitSource: 'HKQuantityTypeIdentifierActiveEnergyBurned',
235
+ isHealthKit: true,
168
236
  availableFrom: { watch: true, manual: true },
237
+ formatter: (s) => String(s.calories),
238
+ condition: (s) => !!s.calories,
169
239
  },
170
240
  totalStrokeCount: {
171
241
  key: 'totalStrokeCount',
172
242
  label: 'Total Strokes',
173
243
  unit: '',
174
244
  category: MetricCategory.TECHNIQUE,
175
- description: 'Total strokes',
245
+ description: 'Total number of arm strokes taken during the session.',
176
246
  icon: 'hand-left-outline',
177
247
  isPerSession: true,
178
- source: 'Apple Watch accelerometer and gyroscope stroke detection',
248
+ source: 'HealthKit: HKQuantityTypeIdentifierSwimmingStrokeCount',
249
+ healthKitSource: 'HKQuantityTypeIdentifierSwimmingStrokeCount',
250
+ isHealthKit: true,
179
251
  availableFrom: { watch: true, manual: false },
252
+ formatter: (s) => String(s.totalStrokeCount || 0),
253
+ condition: (s) => !!s.totalStrokeCount,
180
254
  },
181
255
  avgStrokeRate: {
182
256
  key: 'avgStrokeRate',
183
257
  label: 'Stroke Rate',
184
258
  unit: 'SPM',
185
259
  category: MetricCategory.TECHNIQUE,
186
- description: 'Strokes per minute',
260
+ description: 'Average number of strokes per minute throughout the session.',
187
261
  icon: 'repeat-outline',
188
262
  isPerSession: true,
189
263
  formula: 'Total Strokes ÷ (Active Time in Minutes)',
190
264
  availableFrom: { watch: true, manual: true },
265
+ formatter: (s) => `${(0, exports.formatNumber)(s.avgStrokeRate, 1)} SPM`,
266
+ condition: (s) => !!s.avgStrokeRate,
191
267
  },
192
268
  avgDistancePerStroke: {
193
269
  key: 'avgDistancePerStroke',
194
270
  label: 'Distance/Stroke',
195
271
  unit: 'm',
196
272
  category: MetricCategory.TECHNIQUE,
197
- description: 'Distance per stroke',
273
+ description: 'Average distance covered per stroke - indicates stroke efficiency.',
198
274
  icon: 'arrow-forward-outline',
199
275
  isPerSession: true,
200
276
  formula: 'Distance ÷ Total Strokes',
201
277
  availableFrom: { watch: true, manual: true },
278
+ formatter: (s) => `${(0, exports.formatNumber)(s.avgDistancePerStroke, 2)}m`,
279
+ condition: (s) => !!s.avgDistancePerStroke,
202
280
  },
203
281
  avgSwolfScore: {
204
282
  key: 'avgSwolfScore',
205
- label: 'SWOLF',
283
+ label: 'SWOLF Score',
206
284
  unit: '',
207
285
  category: MetricCategory.TECHNIQUE,
208
- description: 'Efficiency score',
286
+ description: 'Swimming efficiency score combining time and stroke count per length.',
209
287
  icon: 'trophy-outline',
210
288
  isPerSession: true,
211
289
  formula: 'Average of (Lap Time in Seconds + Lap Stroke Count) for each lap',
212
290
  availableFrom: { watch: true, manual: true },
291
+ formatter: (s) => (0, exports.formatNumber)(s.avgSwolfScore, 0),
292
+ condition: (s) => !!s.avgSwolfScore,
213
293
  },
214
294
  primaryStrokeType: {
215
295
  key: 'primaryStrokeType',
216
296
  label: 'Primary Stroke',
217
297
  unit: '',
218
298
  category: MetricCategory.TECHNIQUE,
219
- description: 'Main stroke',
299
+ description: 'Main swimming stroke used during this session.',
220
300
  icon: 'hand-right-outline',
221
301
  isPerSession: true,
222
- source: 'Apple Watch motion pattern recognition',
302
+ source: 'HealthKit: HKWorkoutEvent with HKMetadataKeySwimmingStrokeStyle',
303
+ healthKitSource: 'HKWorkoutEvent.metadata[HKMetadataKeySwimmingStrokeStyle]',
304
+ isHealthKit: true,
223
305
  availableFrom: { watch: true, manual: true },
306
+ formatter: (s) => s.primaryStrokeType,
307
+ condition: (s) => !!s.primaryStrokeType,
224
308
  },
225
309
  lapSplitTimes: {
226
310
  key: 'lapSplitTimes',
227
- label: 'Split Times',
311
+ label: 'Time',
228
312
  unit: 's',
229
313
  category: MetricCategory.PERFORMANCE,
230
- description: 'Lap times',
314
+ description: 'Individual lap times recorded during the session.',
231
315
  icon: 'stopwatch-outline',
232
316
  isPerLap: true,
233
317
  source: 'Tracked per lap completion',
234
318
  availableFrom: { watch: true, manual: false },
319
+ formatter: (s) => `${s.lapSplitTimes.length} splits`,
320
+ lapFormatter: (v) => (0, exports.formatTime)(v),
321
+ condition: (s) => !!s.lapSplitTimes?.length,
235
322
  },
236
323
  lapHeartRates: {
237
324
  key: 'lapHeartRates',
238
- label: 'Lap HR',
325
+ label: 'HR',
239
326
  unit: 'bpm',
240
327
  category: MetricCategory.HEALTH,
241
328
  description: 'HR per lap',
242
329
  icon: 'heart-outline',
243
330
  isPerLap: true,
331
+ decimals: 0,
244
332
  source: 'HealthKit: HKQuantityTypeIdentifierHeartRate sampled at lap completion',
245
333
  healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
334
+ isHealthKit: true,
246
335
  availableFrom: { watch: true, manual: false },
247
336
  },
248
337
  lapStrokeCounts: {
249
338
  key: 'lapStrokeCounts',
250
- label: 'Lap Strokes',
339
+ label: 'Strokes',
251
340
  unit: '',
252
341
  category: MetricCategory.TECHNIQUE,
253
342
  description: 'Strokes per lap',
254
343
  icon: 'hand-left-outline',
255
344
  isPerLap: true,
256
- source: 'Apple Watch accelerometer stroke detection per lap',
345
+ decimals: 0,
346
+ source: 'HealthKit: HKQuantityTypeIdentifierSwimmingStrokeCount per lap',
347
+ healthKitSource: 'HKQuantityTypeIdentifierSwimmingStrokeCount',
348
+ isHealthKit: true,
257
349
  availableFrom: { watch: true, manual: false },
258
350
  },
259
351
  id: {
@@ -290,37 +382,46 @@ exports.METRICS_REGISTRY = {
290
382
  },
291
383
  lapTurnTimes: {
292
384
  key: 'lapTurnTimes',
293
- label: 'Turn Times',
385
+ label: 'Turn',
294
386
  unit: 's',
295
387
  category: MetricCategory.TECHNIQUE,
296
- description: 'Turn times per lap',
388
+ description: 'Time spent turning at pool wall per lap',
389
+ icon: 'refresh-circle-outline',
297
390
  isPerLap: true,
391
+ decimals: 2,
298
392
  availableFrom: { watch: true, manual: false },
393
+ formatter: exports.formatAverageTurnTime,
394
+ condition: (s) => !!s.lapTurnTimes?.length,
299
395
  },
300
396
  lapStrokeTypes: {
301
397
  key: 'lapStrokeTypes',
302
- label: 'Stroke Types',
398
+ label: 'Stroke',
303
399
  category: MetricCategory.TECHNIQUE,
304
- description: 'Stroke types per lap',
400
+ description: 'Swimming stroke used per lap',
401
+ icon: 'list-outline',
305
402
  isPerLap: true,
306
403
  availableFrom: { watch: true, manual: false },
404
+ formatter: (s) => `${s.lapStrokeTypes.length} types`,
405
+ condition: (s) => !!s.lapStrokeTypes?.length,
307
406
  },
308
407
  lapPaces: {
309
408
  key: 'lapPaces',
310
- label: 'Lap Paces',
409
+ label: 'Pace',
311
410
  unit: '/100m',
312
411
  category: MetricCategory.PERFORMANCE,
313
412
  description: 'Pace per lap',
314
413
  isPerLap: true,
414
+ decimals: 1,
315
415
  availableFrom: { watch: true, manual: false },
316
416
  },
317
417
  lapCalories: {
318
418
  key: 'lapCalories',
319
- label: 'Lap Calories',
419
+ label: 'Cal',
320
420
  unit: 'kcal',
321
421
  category: MetricCategory.ENERGY,
322
422
  description: 'Calories per lap',
323
423
  isPerLap: true,
424
+ decimals: 0,
324
425
  availableFrom: { watch: true, manual: false },
325
426
  },
326
427
  lapDistances: {
@@ -334,31 +435,34 @@ exports.METRICS_REGISTRY = {
334
435
  },
335
436
  lapSwolfScores: {
336
437
  key: 'lapSwolfScores',
337
- label: 'Lap SWOLF',
438
+ label: 'SWOLF',
338
439
  category: MetricCategory.TECHNIQUE,
339
440
  description: 'SWOLF per lap',
340
441
  isPerLap: true,
442
+ decimals: 0,
341
443
  availableFrom: { watch: true, manual: false },
342
444
  },
343
445
  lapStrokeRates: {
344
446
  key: 'lapStrokeRates',
345
- label: 'Lap Stroke Rate',
447
+ label: 'SR',
346
448
  unit: 'SPM',
347
449
  category: MetricCategory.TECHNIQUE,
348
450
  description: 'Strokes per minute per lap',
349
451
  icon: 'repeat-outline',
350
452
  isPerLap: true,
453
+ decimals: 1,
351
454
  formula: 'Lap Strokes ÷ (Lap Time in Minutes)',
352
455
  availableFrom: { watch: true, manual: false },
353
456
  },
354
457
  lapDistancesPerStroke: {
355
458
  key: 'lapDistancesPerStroke',
356
- label: 'Lap Distance/Stroke',
459
+ label: 'DPS',
357
460
  unit: 'm',
358
461
  category: MetricCategory.TECHNIQUE,
359
462
  description: 'Distance covered per stroke per lap',
360
463
  icon: 'arrow-forward-outline',
361
464
  isPerLap: true,
465
+ decimals: 2,
362
466
  formula: 'Lap Distance ÷ Lap Strokes',
363
467
  availableFrom: { watch: true, manual: false },
364
468
  },
@@ -382,25 +486,34 @@ exports.METRICS_REGISTRY = {
382
486
  key: 'trainingTypes',
383
487
  label: 'Training Types',
384
488
  category: MetricCategory.PERFORMANCE,
385
- description: 'Training types',
489
+ description: 'Types of training focus for this swimming session.',
490
+ icon: 'school-outline',
386
491
  isPerSession: true,
387
492
  availableFrom: { watch: false, manual: true },
493
+ formatter: (s) => s.trainingTypes?.join(', ') || '',
494
+ condition: (s) => !!s.trainingTypes?.length,
388
495
  },
389
496
  perceivedEffort: {
390
497
  key: 'perceivedEffort',
391
498
  label: 'Perceived Effort',
392
- category: MetricCategory.PERFORMANCE,
393
- description: 'Effort rating',
499
+ category: MetricCategory.HEALTH,
500
+ description: 'Subjective rating of how hard the session felt.',
501
+ icon: 'body-outline',
394
502
  isPerSession: true,
395
503
  availableFrom: { watch: false, manual: true },
504
+ formatter: (s) => s.perceivedEffort,
505
+ condition: (s) => !!s.perceivedEffort,
396
506
  },
397
507
  equipment: {
398
508
  key: 'equipment',
399
509
  label: 'Equipment',
400
- category: MetricCategory.PERFORMANCE,
401
- description: 'Equipment used',
510
+ category: MetricCategory.TECHNIQUE,
511
+ description: 'Swimming equipment used during the session.',
512
+ icon: 'construct-outline',
402
513
  isPerSession: true,
403
514
  availableFrom: { watch: false, manual: true },
515
+ formatter: (s) => s.equipment?.join(', ') || '',
516
+ condition: (s) => !!s.equipment?.length,
404
517
  },
405
518
  };
406
519
  exports.MetricsHelper = {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("./index");
4
+ describe('Metrics Formatting Functions', () => {
5
+ describe('formatNumber', () => {
6
+ it('should format number with default 2 decimals', () => {
7
+ expect((0, index_1.formatNumber)(123.456)).toBe('123.46');
8
+ });
9
+ it('should format number with custom decimals', () => {
10
+ expect((0, index_1.formatNumber)(123.456, 1)).toBe('123.5');
11
+ expect((0, index_1.formatNumber)(123.456, 0)).toBe('123');
12
+ expect((0, index_1.formatNumber)(123.456, 3)).toBe('123.456');
13
+ });
14
+ it('should handle integers', () => {
15
+ expect((0, index_1.formatNumber)(100, 2)).toBe('100.00');
16
+ });
17
+ });
18
+ describe('formatPace', () => {
19
+ it('should return string pace as-is', () => {
20
+ expect((0, index_1.formatPace)('2:30')).toBe('2:30');
21
+ });
22
+ it('should format numeric pace in seconds to mm:ss', () => {
23
+ expect((0, index_1.formatPace)(150)).toBe('2:30');
24
+ expect((0, index_1.formatPace)(90)).toBe('1:30');
25
+ expect((0, index_1.formatPace)(65)).toBe('1:05');
26
+ });
27
+ it('should handle zero seconds', () => {
28
+ expect((0, index_1.formatPace)(0)).toBe('0:00');
29
+ });
30
+ it('should pad seconds with leading zero', () => {
31
+ expect((0, index_1.formatPace)(125)).toBe('2:05');
32
+ });
33
+ });
34
+ describe('formatTime', () => {
35
+ it('should format time in seconds to mm:ss', () => {
36
+ expect((0, index_1.formatTime)(125)).toBe('2:05');
37
+ expect((0, index_1.formatTime)(90)).toBe('1:30');
38
+ expect((0, index_1.formatTime)(3665)).toBe('61:05');
39
+ });
40
+ it('should handle zero', () => {
41
+ expect((0, index_1.formatTime)(0)).toBe('0:00');
42
+ });
43
+ it('should pad seconds with leading zero', () => {
44
+ expect((0, index_1.formatTime)(65)).toBe('1:05');
45
+ expect((0, index_1.formatTime)(5)).toBe('0:05');
46
+ });
47
+ it('should handle fractional seconds by flooring', () => {
48
+ expect((0, index_1.formatTime)(65.7)).toBe('1:05');
49
+ });
50
+ });
51
+ describe('formatHeartRate', () => {
52
+ it('should format heart rate with bpm unit', () => {
53
+ expect((0, index_1.formatHeartRate)(145)).toBe('145 bpm');
54
+ expect((0, index_1.formatHeartRate)(72.5)).toBe('73 bpm');
55
+ });
56
+ it('should round to nearest integer', () => {
57
+ expect((0, index_1.formatHeartRate)(145.4)).toBe('145 bpm');
58
+ expect((0, index_1.formatHeartRate)(145.6)).toBe('146 bpm');
59
+ });
60
+ });
61
+ describe('formatDistance', () => {
62
+ it('should format meters for distances under 1000m', () => {
63
+ expect((0, index_1.formatDistance)(500)).toBe('500m');
64
+ expect((0, index_1.formatDistance)(999)).toBe('999m');
65
+ });
66
+ it('should format kilometers for distances 1000m and above', () => {
67
+ expect((0, index_1.formatDistance)(1000)).toBe('1.0km');
68
+ expect((0, index_1.formatDistance)(2500)).toBe('2.5km');
69
+ expect((0, index_1.formatDistance)(1234)).toBe('1.2km');
70
+ });
71
+ it('should round meters to nearest integer', () => {
72
+ expect((0, index_1.formatDistance)(500.7)).toBe('501m');
73
+ });
74
+ });
75
+ describe('formatAverageTurnTime', () => {
76
+ const createSession = (lapTurnTimes) => ({
77
+ id: '1',
78
+ date: '2024-01-01',
79
+ distance: 1000,
80
+ laps: 20,
81
+ time: 1200,
82
+ calories: 300,
83
+ avgPace: '2:00',
84
+ poolLength: 50,
85
+ source: 'watch',
86
+ lapTurnTimes,
87
+ });
88
+ it('should return empty string when no lap turn times', () => {
89
+ expect((0, index_1.formatAverageTurnTime)(createSession())).toBe('');
90
+ expect((0, index_1.formatAverageTurnTime)(createSession([]))).toBe('');
91
+ });
92
+ it('should calculate average turn time', () => {
93
+ expect((0, index_1.formatAverageTurnTime)(createSession([2.5, 3.0, 2.5]))).toBe('2.7s');
94
+ });
95
+ it('should filter out invalid turn times', () => {
96
+ expect((0, index_1.formatAverageTurnTime)(createSession([2.5, NaN, 3.0, 0, 2.5]))).toBe('2.7s');
97
+ });
98
+ it('should return empty string when all turn times are invalid', () => {
99
+ expect((0, index_1.formatAverageTurnTime)(createSession([NaN, 0, -1]))).toBe('');
100
+ });
101
+ it('should format with 1 decimal place', () => {
102
+ expect((0, index_1.formatAverageTurnTime)(createSession([2.456]))).toBe('2.5s');
103
+ });
104
+ });
105
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimedge/metrics",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Shared metrics registry for SwimEdge (frontend, backend, watch)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,7 +16,10 @@
16
16
  "prepublishOnly": "npm run build",
17
17
  "build": "tsc",
18
18
  "watch": "tsc --watch",
19
- "export-json": "node scripts/export-json.js"
19
+ "export-json": "node scripts/export-json.js",
20
+ "test": "jest",
21
+ "test:watch": "jest --watch",
22
+ "test:coverage": "jest --coverage"
20
23
  },
21
24
  "keywords": [
22
25
  "metrics",
@@ -28,8 +31,26 @@
28
31
  "engines": {
29
32
  "node": ">=18.0.0"
30
33
  },
34
+ "jest": {
35
+ "preset": "ts-jest",
36
+ "testEnvironment": "node",
37
+ "roots": [
38
+ "<rootDir>/src"
39
+ ],
40
+ "testMatch": [
41
+ "**/*.spec.ts"
42
+ ],
43
+ "collectCoverageFrom": [
44
+ "src/**/*.ts",
45
+ "!src/**/*.spec.ts",
46
+ "!src/**/*.d.ts"
47
+ ]
48
+ },
31
49
  "devDependencies": {
50
+ "@types/jest": "^29.5.0",
32
51
  "esbuild-register": "^3.6.0",
52
+ "jest": "^29.7.0",
53
+ "ts-jest": "^29.1.0",
33
54
  "typescript": "^5.0.0"
34
55
  }
35
- }
56
+ }