@swimedge/metrics 1.1.1 → 1.1.3

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
@@ -53,6 +53,7 @@ export interface SwimSession {
53
53
  avgDistancePerStroke?: number;
54
54
  routeCoordinates?: number[][];
55
55
  routeMapImage?: string;
56
+ routeMapThumbnail?: string;
56
57
  deviceType?: string;
57
58
  osVersion?: string;
58
59
  healthKitWorkoutId?: string;
package/dist/index.js CHANGED
@@ -37,7 +37,9 @@ const formatTime = (seconds) => {
37
37
  if (mins >= 60) {
38
38
  const hours = Math.floor(mins / 60);
39
39
  const remainingMins = mins % 60;
40
- return `${hours}:${remainingMins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
40
+ return `${hours}:${remainingMins.toString().padStart(2, '0')}:${secs
41
+ .toString()
42
+ .padStart(2, '0')}`;
41
43
  }
42
44
  return `${mins}:${secs.toString().padStart(2, '0')}`;
43
45
  };
@@ -90,7 +92,7 @@ exports.LAP_METRICS_REGISTRY = {
90
92
  label: 'Pace',
91
93
  unit: '/100m',
92
94
  category: MetricCategory.PERFORMANCE,
93
- description: 'Time per 100m for this lap',
95
+ description: 'Time per 100m per lap',
94
96
  decimals: 1,
95
97
  availableFrom: { watch: true, manual: false },
96
98
  swimTypes: [SwimType.INDOOR],
@@ -101,7 +103,7 @@ exports.LAP_METRICS_REGISTRY = {
101
103
  label: 'SR',
102
104
  unit: 'SPM',
103
105
  category: MetricCategory.TECHNIQUE,
104
- description: 'Strokes per minute for this lap',
106
+ description: 'Strokes per minute per lap',
105
107
  icon: 'repeat-outline',
106
108
  decimals: 1,
107
109
  formula: 'Lap Strokes ÷ (Lap Time in Minutes)',
@@ -113,7 +115,7 @@ exports.LAP_METRICS_REGISTRY = {
113
115
  label: 'DPS',
114
116
  unit: 'm',
115
117
  category: MetricCategory.TECHNIQUE,
116
- description: 'Distance per stroke for this lap',
118
+ description: 'Distance per stroke per lap',
117
119
  icon: 'arrow-forward-outline',
118
120
  decimals: 2,
119
121
  formula: 'Lap Distance ÷ Lap Strokes',
@@ -124,7 +126,7 @@ exports.LAP_METRICS_REGISTRY = {
124
126
  key: 'strokeStyle',
125
127
  label: 'Stroke',
126
128
  category: MetricCategory.TECHNIQUE,
127
- description: 'Swimming stroke used in this lap',
129
+ description: 'Swimming stroke used per lap',
128
130
  icon: 'list-outline',
129
131
  availableFrom: { watch: true, manual: false },
130
132
  swimTypes: [SwimType.INDOOR],
@@ -134,7 +136,7 @@ exports.LAP_METRICS_REGISTRY = {
134
136
  label: 'Time',
135
137
  unit: 's',
136
138
  category: MetricCategory.PERFORMANCE,
137
- description: 'Time taken for this lap',
139
+ description: 'Time taken per lap',
138
140
  icon: 'stopwatch-outline',
139
141
  source: 'Tracked per lap completion',
140
142
  availableFrom: { watch: true, manual: false },
@@ -145,7 +147,7 @@ exports.LAP_METRICS_REGISTRY = {
145
147
  key: 'swolfScore',
146
148
  label: 'SWOLF',
147
149
  category: MetricCategory.TECHNIQUE,
148
- description: 'SWOLF score for this lap',
150
+ description: 'SWOLF score per lap',
149
151
  decimals: 0,
150
152
  availableFrom: { watch: true, manual: false },
151
153
  swimTypes: [SwimType.INDOOR],
@@ -353,7 +355,7 @@ exports.METRICS_REGISTRY = {
353
355
  key: 'calories',
354
356
  label: 'Calories',
355
357
  unit: 'kcal',
356
- category: MetricCategory.ENERGY,
358
+ category: MetricCategory.HEALTH,
357
359
  description: 'Total calories burned during the swimming session.',
358
360
  icon: 'flame-outline',
359
361
  isPerSession: true,
@@ -363,7 +365,7 @@ exports.METRICS_REGISTRY = {
363
365
  availableFrom: { watch: true, manual: true },
364
366
  swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
365
367
  hasChart: true,
366
- formatter: (s) => String(s.calories),
368
+ formatter: (s) => s.calories,
367
369
  condition: (s) => !!s.calories,
368
370
  },
369
371
  totalStrokeCount: {
@@ -524,9 +526,11 @@ exports.METRICS_REGISTRY = {
524
526
  2: 'Easy',
525
527
  3: 'Moderate',
526
528
  4: 'Hard',
527
- 5: 'Very Hard'
529
+ 5: 'Very Hard',
528
530
  };
529
- const effortNum = typeof s.perceivedEffort === 'string' ? parseInt(s.perceivedEffort) : s.perceivedEffort;
531
+ const effortNum = typeof s.perceivedEffort === 'string'
532
+ ? parseInt(s.perceivedEffort)
533
+ : s.perceivedEffort;
530
534
  return effortMap[effortNum] || 'Unknown';
531
535
  },
532
536
  condition: (s) => !!s.perceivedEffort,
@@ -14,6 +14,18 @@ describe('Metrics Formatting Functions', () => {
14
14
  it('should handle integers', () => {
15
15
  expect((0, index_1.formatNumber)(100, 2)).toBe('100.00');
16
16
  });
17
+ it('should handle negative numbers', () => {
18
+ expect((0, index_1.formatNumber)(-123.456, 2)).toBe('-123.46');
19
+ });
20
+ it('should handle zero', () => {
21
+ expect((0, index_1.formatNumber)(0, 2)).toBe('0.00');
22
+ });
23
+ it('should handle very small numbers', () => {
24
+ expect((0, index_1.formatNumber)(0.001, 3)).toBe('0.001');
25
+ });
26
+ it('should handle very large numbers', () => {
27
+ expect((0, index_1.formatNumber)(999999.99, 2)).toBe('999999.99');
28
+ });
17
29
  });
18
30
  describe('formatPace', () => {
19
31
  it('should return string pace as-is', () => {
@@ -30,12 +42,21 @@ describe('Metrics Formatting Functions', () => {
30
42
  it('should pad seconds with leading zero', () => {
31
43
  expect((0, index_1.formatPace)(125)).toBe('2:05');
32
44
  });
45
+ it('should handle large values', () => {
46
+ expect((0, index_1.formatPace)(3665)).toBe('61:05');
47
+ });
48
+ it('should handle single digit seconds', () => {
49
+ expect((0, index_1.formatPace)(61)).toBe('1:01');
50
+ });
33
51
  });
34
52
  describe('formatTime', () => {
35
53
  it('should format time in seconds to mm:ss', () => {
36
54
  expect((0, index_1.formatTime)(125)).toBe('2:05');
37
55
  expect((0, index_1.formatTime)(90)).toBe('1:30');
56
+ });
57
+ it('should format time with hours', () => {
38
58
  expect((0, index_1.formatTime)(3665)).toBe('1:01:05');
59
+ expect((0, index_1.formatTime)(7200)).toBe('2:00:00');
39
60
  });
40
61
  it('should handle zero', () => {
41
62
  expect((0, index_1.formatTime)(0)).toBe('0:00');
@@ -44,9 +65,18 @@ describe('Metrics Formatting Functions', () => {
44
65
  expect((0, index_1.formatTime)(65)).toBe('1:05');
45
66
  expect((0, index_1.formatTime)(5)).toBe('0:05');
46
67
  });
68
+ it('should pad minutes with leading zero in hour format', () => {
69
+ expect((0, index_1.formatTime)(3605)).toBe('1:00:05');
70
+ });
47
71
  it('should handle fractional seconds by flooring', () => {
48
72
  expect((0, index_1.formatTime)(65.7)).toBe('1:05');
49
73
  });
74
+ it('should handle exactly 60 seconds', () => {
75
+ expect((0, index_1.formatTime)(60)).toBe('1:00');
76
+ });
77
+ it('should handle exactly 1 hour', () => {
78
+ expect((0, index_1.formatTime)(3600)).toBe('1:00:00');
79
+ });
50
80
  });
51
81
  describe('formatHeartRate', () => {
52
82
  it('should format heart rate with bpm unit', () => {
@@ -57,6 +87,12 @@ describe('Metrics Formatting Functions', () => {
57
87
  expect((0, index_1.formatHeartRate)(145.4)).toBe('145 bpm');
58
88
  expect((0, index_1.formatHeartRate)(145.6)).toBe('146 bpm');
59
89
  });
90
+ it('should handle zero', () => {
91
+ expect((0, index_1.formatHeartRate)(0)).toBe('0 bpm');
92
+ });
93
+ it('should handle high heart rates', () => {
94
+ expect((0, index_1.formatHeartRate)(200)).toBe('200 bpm');
95
+ });
60
96
  });
61
97
  describe('formatDistance', () => {
62
98
  it('should format meters for distances under 1000m', () => {
@@ -71,5 +107,208 @@ describe('Metrics Formatting Functions', () => {
71
107
  it('should round meters to nearest integer', () => {
72
108
  expect((0, index_1.formatDistance)(500.7)).toBe('501m');
73
109
  });
110
+ it('should handle zero', () => {
111
+ expect((0, index_1.formatDistance)(0)).toBe('0m');
112
+ });
113
+ it('should handle very large distances', () => {
114
+ expect((0, index_1.formatDistance)(10000)).toBe('10.0km');
115
+ });
116
+ });
117
+ describe('formatSwimType', () => {
118
+ it('should format indoor pool type', () => {
119
+ expect((0, index_1.formatSwimType)(index_1.SwimType.INDOOR)).toBe('Indoor Pool');
120
+ expect((0, index_1.formatSwimType)('indoorPool')).toBe('Indoor Pool');
121
+ });
122
+ it('should format open water type', () => {
123
+ expect((0, index_1.formatSwimType)(index_1.SwimType.OPEN_WATER)).toBe('Open Water');
124
+ expect((0, index_1.formatSwimType)('openWater')).toBe('Open Water');
125
+ });
126
+ it('should handle undefined', () => {
127
+ expect((0, index_1.formatSwimType)(undefined)).toBe('Unknown');
128
+ });
129
+ it('should return unknown type as-is', () => {
130
+ expect((0, index_1.formatSwimType)('unknown')).toBe('unknown');
131
+ });
132
+ });
133
+ });
134
+ describe('MetricsHelper', () => {
135
+ describe('getMetric', () => {
136
+ it('should return metric definition by key', () => {
137
+ const metric = index_1.MetricsHelper.getMetric('distance');
138
+ expect(metric).toBeDefined();
139
+ expect(metric?.key).toBe('distance');
140
+ expect(metric?.label).toBe('Distance');
141
+ });
142
+ it('should return undefined for non-existent key', () => {
143
+ const metric = index_1.MetricsHelper.getMetric('nonexistent');
144
+ expect(metric).toBeUndefined();
145
+ });
146
+ });
147
+ describe('getSessionMetrics', () => {
148
+ it('should return all session metrics', () => {
149
+ const metrics = index_1.MetricsHelper.getSessionMetrics();
150
+ expect(metrics.length).toBeGreaterThan(0);
151
+ expect(metrics.every(m => m.isPerSession)).toBe(true);
152
+ });
153
+ });
154
+ describe('getChartMetrics', () => {
155
+ it('should return only chartable metrics', () => {
156
+ const metrics = index_1.MetricsHelper.getChartMetrics();
157
+ expect(metrics.length).toBeGreaterThan(0);
158
+ expect(metrics.every(m => m.hasChart && m.isPerSession)).toBe(true);
159
+ });
160
+ });
161
+ describe('getLapMetrics', () => {
162
+ it('should return all lap metrics', () => {
163
+ const metrics = index_1.MetricsHelper.getLapMetrics();
164
+ expect(metrics.length).toBeGreaterThan(0);
165
+ expect(metrics).toContainEqual(expect.objectContaining({ key: 'pace' }));
166
+ });
167
+ });
168
+ describe('getLapMetric', () => {
169
+ it('should return lap metric by key', () => {
170
+ const metric = index_1.MetricsHelper.getLapMetric('pace');
171
+ expect(metric).toBeDefined();
172
+ expect(metric.key).toBe('pace');
173
+ expect(metric.label).toBe('Pace');
174
+ });
175
+ });
176
+ describe('getMetricsByCategory', () => {
177
+ it('should return metrics by performance category', () => {
178
+ const metrics = index_1.MetricsHelper.getMetricsByCategory(index_1.MetricCategory.PERFORMANCE);
179
+ expect(metrics.length).toBeGreaterThan(0);
180
+ expect(metrics.every(m => m.category === index_1.MetricCategory.PERFORMANCE)).toBe(true);
181
+ });
182
+ it('should return metrics by health category', () => {
183
+ const metrics = index_1.MetricsHelper.getMetricsByCategory(index_1.MetricCategory.HEALTH);
184
+ expect(metrics.length).toBeGreaterThan(0);
185
+ expect(metrics.every(m => m.category === index_1.MetricCategory.HEALTH)).toBe(true);
186
+ });
187
+ it('should return metrics by technique category', () => {
188
+ const metrics = index_1.MetricsHelper.getMetricsByCategory(index_1.MetricCategory.TECHNIQUE);
189
+ expect(metrics.length).toBeGreaterThan(0);
190
+ expect(metrics.every(m => m.category === index_1.MetricCategory.TECHNIQUE)).toBe(true);
191
+ });
192
+ });
193
+ describe('getMetricsBySwimType', () => {
194
+ it('should return indoor pool metrics', () => {
195
+ const metrics = index_1.MetricsHelper.getMetricsBySwimType(index_1.SwimType.INDOOR);
196
+ expect(metrics.length).toBeGreaterThan(0);
197
+ expect(metrics.every(m => m.swimTypes.includes(index_1.SwimType.INDOOR))).toBe(true);
198
+ });
199
+ it('should return open water metrics', () => {
200
+ const metrics = index_1.MetricsHelper.getMetricsBySwimType(index_1.SwimType.OPEN_WATER);
201
+ expect(metrics.length).toBeGreaterThan(0);
202
+ expect(metrics.every(m => m.swimTypes.includes(index_1.SwimType.OPEN_WATER))).toBe(true);
203
+ });
204
+ });
205
+ describe('getIndoorMetrics', () => {
206
+ it('should return only indoor metrics', () => {
207
+ const metrics = index_1.MetricsHelper.getIndoorMetrics();
208
+ expect(metrics.length).toBeGreaterThan(0);
209
+ expect(metrics.every(m => m?.swimTypes?.includes(index_1.SwimType.INDOOR))).toBe(true);
210
+ });
211
+ });
212
+ describe('getOpenWaterMetrics', () => {
213
+ it('should return only open water metrics', () => {
214
+ const metrics = index_1.MetricsHelper.getOpenWaterMetrics();
215
+ expect(metrics.length).toBeGreaterThan(0);
216
+ expect(metrics.every(m => m?.swimTypes?.includes(index_1.SwimType.OPEN_WATER))).toBe(true);
217
+ });
218
+ });
219
+ describe('getAllMetricKeys', () => {
220
+ it('should return all metric keys', () => {
221
+ const keys = index_1.MetricsHelper.getAllMetricKeys();
222
+ expect(keys.length).toBeGreaterThan(0);
223
+ expect(keys).toContain('distance');
224
+ expect(keys).toContain('time');
225
+ });
226
+ });
227
+ describe('getAllLapMetricKeys', () => {
228
+ it('should return all lap metric keys', () => {
229
+ const keys = index_1.MetricsHelper.getAllLapMetricKeys();
230
+ expect(keys.length).toBeGreaterThan(0);
231
+ expect(keys).toContain('pace');
232
+ expect(keys).toContain('strokeCount');
233
+ });
234
+ });
235
+ });
236
+ describe('LAP_METRICS_REGISTRY', () => {
237
+ it('should have all required lap metrics', () => {
238
+ expect(index_1.LAP_METRICS_REGISTRY.lapNumber).toBeDefined();
239
+ expect(index_1.LAP_METRICS_REGISTRY.strokeCount).toBeDefined();
240
+ expect(index_1.LAP_METRICS_REGISTRY.pace).toBeDefined();
241
+ expect(index_1.LAP_METRICS_REGISTRY.strokeRate).toBeDefined();
242
+ expect(index_1.LAP_METRICS_REGISTRY.distancePerStroke).toBeDefined();
243
+ expect(index_1.LAP_METRICS_REGISTRY.strokeStyle).toBeDefined();
244
+ expect(index_1.LAP_METRICS_REGISTRY.splitTime).toBeDefined();
245
+ expect(index_1.LAP_METRICS_REGISTRY.swolfScore).toBeDefined();
246
+ });
247
+ it('should have correct descriptions using "per lap"', () => {
248
+ expect(index_1.LAP_METRICS_REGISTRY.pace.description).toBe('Time per 100m per lap');
249
+ expect(index_1.LAP_METRICS_REGISTRY.strokeRate.description).toBe('Strokes per minute per lap');
250
+ expect(index_1.LAP_METRICS_REGISTRY.distancePerStroke.description).toBe('Distance per stroke per lap');
251
+ expect(index_1.LAP_METRICS_REGISTRY.splitTime.description).toBe('Time taken per lap');
252
+ expect(index_1.LAP_METRICS_REGISTRY.swolfScore.description).toBe('SWOLF score per lap');
253
+ });
254
+ it('should have correct categories', () => {
255
+ expect(index_1.LAP_METRICS_REGISTRY.pace.category).toBe(index_1.MetricCategory.PERFORMANCE);
256
+ expect(index_1.LAP_METRICS_REGISTRY.strokeCount.category).toBe(index_1.MetricCategory.TECHNIQUE);
257
+ expect(index_1.LAP_METRICS_REGISTRY.strokeRate.category).toBe(index_1.MetricCategory.TECHNIQUE);
258
+ });
259
+ it('should have correct swim types', () => {
260
+ expect(index_1.LAP_METRICS_REGISTRY.pace.swimTypes).toEqual([index_1.SwimType.INDOOR]);
261
+ expect(index_1.LAP_METRICS_REGISTRY.strokeCount.swimTypes).toEqual([index_1.SwimType.INDOOR]);
262
+ });
263
+ it('should have lap formatters where needed', () => {
264
+ expect(index_1.LAP_METRICS_REGISTRY.pace.lapFormatter).toBeDefined();
265
+ expect(index_1.LAP_METRICS_REGISTRY.splitTime.lapFormatter).toBeDefined();
266
+ });
267
+ });
268
+ describe('METRICS_REGISTRY', () => {
269
+ it('should have all core session metrics', () => {
270
+ expect(index_1.METRICS_REGISTRY.distance).toBeDefined();
271
+ expect(index_1.METRICS_REGISTRY.time).toBeDefined();
272
+ expect(index_1.METRICS_REGISTRY.activeTime).toBeDefined();
273
+ expect(index_1.METRICS_REGISTRY.restTime).toBeDefined();
274
+ expect(index_1.METRICS_REGISTRY.laps).toBeDefined();
275
+ expect(index_1.METRICS_REGISTRY.avgPace).toBeDefined();
276
+ expect(index_1.METRICS_REGISTRY.calories).toBeDefined();
277
+ });
278
+ it('should have all heart rate metrics', () => {
279
+ expect(index_1.METRICS_REGISTRY.avgHeartRate).toBeDefined();
280
+ expect(index_1.METRICS_REGISTRY.maxHeartRate).toBeDefined();
281
+ expect(index_1.METRICS_REGISTRY.minHeartRate).toBeDefined();
282
+ });
283
+ it('should have all stroke metrics', () => {
284
+ expect(index_1.METRICS_REGISTRY.totalStrokeCount).toBeDefined();
285
+ expect(index_1.METRICS_REGISTRY.avgStrokeRate).toBeDefined();
286
+ expect(index_1.METRICS_REGISTRY.avgDistancePerStroke).toBeDefined();
287
+ expect(index_1.METRICS_REGISTRY.avgSwolfScore).toBeDefined();
288
+ });
289
+ it('should have correct HealthKit sources', () => {
290
+ expect(index_1.METRICS_REGISTRY.distance?.isHealthKit).toBe(true);
291
+ expect(index_1.METRICS_REGISTRY.avgHeartRate?.isHealthKit).toBe(true);
292
+ expect(index_1.METRICS_REGISTRY.totalStrokeCount?.isHealthKit).toBe(true);
293
+ });
294
+ it('should have correct swim type availability', () => {
295
+ expect(index_1.METRICS_REGISTRY.laps?.swimTypes).toEqual([index_1.SwimType.INDOOR]);
296
+ expect(index_1.METRICS_REGISTRY.avgSwolfScore?.swimTypes).toEqual([index_1.SwimType.INDOOR]);
297
+ expect(index_1.METRICS_REGISTRY.distance?.swimTypes).toEqual([index_1.SwimType.INDOOR, index_1.SwimType.OPEN_WATER]);
298
+ });
299
+ it('should have formatters for display metrics', () => {
300
+ expect(index_1.METRICS_REGISTRY.distance?.formatter).toBeDefined();
301
+ expect(index_1.METRICS_REGISTRY.time?.formatter).toBeDefined();
302
+ expect(index_1.METRICS_REGISTRY.avgHeartRate?.formatter).toBeDefined();
303
+ });
304
+ it('should have conditions for optional metrics', () => {
305
+ expect(index_1.METRICS_REGISTRY.restTime?.condition).toBeDefined();
306
+ expect(index_1.METRICS_REGISTRY.avgSwolfScore?.condition).toBeDefined();
307
+ expect(index_1.METRICS_REGISTRY.waterTemp?.condition).toBeDefined();
308
+ });
309
+ it('should mark chartable metrics correctly', () => {
310
+ expect(index_1.METRICS_REGISTRY.distance?.hasChart).toBe(true);
311
+ expect(index_1.METRICS_REGISTRY.time?.hasChart).toBe(true);
312
+ expect(index_1.METRICS_REGISTRY.avgPace?.hasChart).toBe(true);
74
313
  });
75
314
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimedge/metrics",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Shared metrics registry for SwimEdge (frontend, backend, watch)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",