hevy-shared 1.0.698 → 1.0.700

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.
@@ -1,3 +1,4 @@
1
+ import Fuse from 'fuse.js';
1
2
  import { EquipmentFilter, Language, LibraryExercise, MuscleGroupFilter, CustomExercise } from './index';
2
3
  export interface SearchableLibraryExercise extends LibraryExercise {
3
4
  localized_muscle_group_name: string;
@@ -20,6 +21,11 @@ interface FilterExercisesProps {
20
21
  searchText: string;
21
22
  language: Language;
22
23
  }
24
+ export declare const createFuseInstance: () => Fuse<string>;
25
+ export declare const newFilterExercises: ({ fuseInstance, exercises, muscleGroupFilter, equipmentType, searchText, language, muscleGroupMatchPenalty, }: FilterExercisesProps & {
26
+ muscleGroupMatchPenalty: number;
27
+ fuseInstance: Fuse<string>;
28
+ }) => SearchableExerciseTemplateWithStringScore[];
23
29
  export declare const filterExercises: ({ exercises, muscleGroupFilter, equipmentType, searchText, language, }: FilterExercisesProps) => (CustomExercise | LibraryExercise)[];
24
30
  /**
25
31
  * Exercise templates that are shown at the top of the Exercise Library to
@@ -1,8 +1,31 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.popularExerciseTemplateIds = exports.filterExercises = void 0;
6
+ exports.popularExerciseTemplateIds = exports.filterExercises = exports.newFilterExercises = exports.createFuseInstance = void 0;
7
+ const fuse_js_1 = __importDefault(require("fuse.js"));
4
8
  const utils_1 = require("./utils");
5
9
  const lodash_1 = require("lodash");
10
+ const createFuseInstance = () => {
11
+ const stringSimilarityFuseInstance = new fuse_js_1.default([''], {
12
+ includeScore: true,
13
+ threshold: 0.4,
14
+ location: 0, // Prefer matches at the beginning of the string
15
+ });
16
+ return stringSimilarityFuseInstance;
17
+ };
18
+ exports.createFuseInstance = createFuseInstance;
19
+ const newFilterExercises = ({ fuseInstance, exercises, muscleGroupFilter, equipmentType, searchText, language, muscleGroupMatchPenalty, }) => {
20
+ let result = exercises;
21
+ result = filterExercisesWithMuscleGroup(muscleGroupFilter, result);
22
+ result = filterExercisesWithEquipment(equipmentType, result);
23
+ const normalizedSearchText = normalizeSearchText(searchText, language);
24
+ result = newFilterExercisesWithSearchText(fuseInstance, normalizedSearchText, result, language, muscleGroupMatchPenalty);
25
+ result = orderExercisesWithSearchText(normalizedSearchText, result);
26
+ return result;
27
+ };
28
+ exports.newFilterExercises = newFilterExercises;
6
29
  const filterExercises = ({ exercises, muscleGroupFilter, equipmentType, searchText, language, }) => {
7
30
  let result = exercises;
8
31
  result = filterExercisesWithMuscleGroup(muscleGroupFilter, result);
@@ -25,6 +48,25 @@ const filterExercisesWithEquipment = (equipmentType, exercises) => {
25
48
  }
26
49
  return exercises.filter((rd) => rd.equipment_category === equipmentType);
27
50
  };
51
+ const fuseScoreToSimilarity = (score) => {
52
+ if (score === 0)
53
+ return 'exact';
54
+ if (score < 0.05)
55
+ return 'extremely-similar';
56
+ if (score < 0.3)
57
+ return 'very-similar';
58
+ if (score < 0.6)
59
+ return 'somewhat-similar';
60
+ return 'not-similar';
61
+ };
62
+ const getStringSimilarityScore = (searchString, targetString, fuseInstance) => {
63
+ var _a, _b;
64
+ fuseInstance.setCollection([targetString]);
65
+ const result = fuseInstance.search(searchString);
66
+ const score = (_b = (_a = result[0]) === null || _a === void 0 ? void 0 : _a.score) !== null && _b !== void 0 ? _b : 1.0;
67
+ return score;
68
+ };
69
+ // Old filter exercise functionality, will be removed when new feature goes live
28
70
  const filterExercisesWithSearchText = (searchText, exercises, language) => {
29
71
  if (searchText.length === 0) {
30
72
  return exercises;
@@ -84,6 +126,89 @@ const filterExercisesWithSearchText = (searchText, exercises, language) => {
84
126
  return false;
85
127
  });
86
128
  };
129
+ const newFilterExercisesWithSearchText = (fuseInstance, searchText, exercises, language,
130
+ // 0 means no penalty, 1 means don't match muscle groups (because a score > 1 means no match)
131
+ muscleGroupMatchPenalty) => {
132
+ if (searchText.length === 0) {
133
+ return exercises;
134
+ }
135
+ return exercises.reduce((accu, e) => {
136
+ const searchableSearchText = searchable(searchText);
137
+ const searchableEnglishTitle = searchable(e.title);
138
+ const searchableLocalizedTitleMap = !e.is_custom
139
+ ? {
140
+ en: searchable(e.title),
141
+ es: e.es_title ? searchable(e.es_title) : undefined,
142
+ de: e.de_title ? searchable(e.de_title) : undefined,
143
+ fr: e.fr_title ? searchable(e.fr_title) : undefined,
144
+ it: e.it_title ? searchable(e.it_title) : undefined,
145
+ pt: e.pt_title ? searchable(e.pt_title) : undefined,
146
+ tr: e.tr_title ? searchable(e.tr_title) : undefined,
147
+ zh_CN: e.zh_cn_title ? searchable(e.zh_cn_title) : undefined,
148
+ zh_TW: e.zh_tw_title ? searchable(e.zh_tw_title) : undefined,
149
+ ru: e.ru_title ? searchable(e.ru_title) : undefined,
150
+ ja: e.ja_title ? searchable(e.ja_title) : undefined,
151
+ ko: e.ko_title ? searchable(e.ko_title) : undefined,
152
+ }
153
+ : {
154
+ en: searchable(e.title),
155
+ };
156
+ const searchableLocalizedTitle = searchableLocalizedTitleMap[language] || searchableLocalizedTitleMap.en;
157
+ const searchableLocalizedMuscleGroupName = searchable(e.localized_muscle_group_name);
158
+ const searchableMuscleGroup = searchable(e.muscle_group);
159
+ const searchableMisspelledDumbbellEquipment = e.equipment_category === 'dumbbell' ? 'dumbell' : '';
160
+ const searchableTags = !e.is_custom && e.manual_tag ? searchable(e.manual_tag) : '';
161
+ const searchableAka = !e.is_custom && e.aka ? searchable(e.aka) : '';
162
+ const englishSimilarityScore = getStringSimilarityScore(searchableSearchText, searchableEnglishTitle, fuseInstance);
163
+ const localizedSimilarityScore = getStringSimilarityScore(searchableSearchText, searchableLocalizedTitle, fuseInstance);
164
+ let englishMuscleGroupSimilarityScore = 1;
165
+ let muscleGroupSimilarityScore = 1;
166
+ if (muscleGroupMatchPenalty < 1) {
167
+ englishMuscleGroupSimilarityScore = getStringSimilarityScore(searchableSearchText, searchableMuscleGroup, fuseInstance);
168
+ muscleGroupSimilarityScore = getStringSimilarityScore(searchableSearchText, searchableLocalizedMuscleGroupName, fuseInstance);
169
+ }
170
+ // We always return the best score, regardless of where the match happens
171
+ const bestScore = Math.min(englishSimilarityScore, localizedSimilarityScore, englishMuscleGroupSimilarityScore + muscleGroupMatchPenalty, muscleGroupSimilarityScore + muscleGroupMatchPenalty);
172
+ if (fuseScoreToSimilarity(englishSimilarityScore) !== 'not-similar') {
173
+ accu.push(Object.assign(Object.assign({}, e), { searchStringScore: bestScore }));
174
+ return accu;
175
+ }
176
+ if (fuseScoreToSimilarity(localizedSimilarityScore) !== 'not-similar') {
177
+ accu.push(Object.assign(Object.assign({}, e), { searchStringScore: bestScore }));
178
+ return accu;
179
+ }
180
+ if (['exact', 'extremely-similar'].includes(fuseScoreToSimilarity(englishMuscleGroupSimilarityScore))) {
181
+ accu.push(Object.assign(Object.assign({}, e), { searchStringScore: bestScore }));
182
+ return accu;
183
+ }
184
+ if (['exact', 'extremely-similar'].includes(fuseScoreToSimilarity(muscleGroupSimilarityScore))) {
185
+ accu.push(Object.assign(Object.assign({}, e), { searchStringScore: bestScore }));
186
+ return accu;
187
+ }
188
+ if (e.is_custom && 'custom'.includes(searchableSearchText)) {
189
+ accu.push(e);
190
+ return accu;
191
+ }
192
+ const splitSearch = searchableSearchText.split(' ');
193
+ const splitTitle = [
194
+ searchableLocalizedTitle,
195
+ searchableEnglishTitle,
196
+ searchableTags,
197
+ searchableAka,
198
+ searchableMisspelledDumbbellEquipment,
199
+ ]
200
+ .join(' ')
201
+ .split(' ');
202
+ // Find:
203
+ // title ['arnold', 'press', '(barbell)']
204
+ // search ['press', 'bar']
205
+ if ((0, lodash_1.intersectionWith)(splitSearch, splitTitle, (search, title) => title.includes(search)).length === splitSearch.length) {
206
+ accu.push(e);
207
+ return accu;
208
+ }
209
+ return accu;
210
+ }, []);
211
+ };
87
212
  const orderExercisesWithSearchText = (searchText, exercises) => {
88
213
  if (searchText.length === 0) {
89
214
  return exercises;
@@ -30,3 +30,8 @@ export type Some<T> = {
30
30
  * ```
31
31
  */
32
32
  export declare const dangerousUncheckedTypeCast: <T = void, U extends T = T>(value: unknown) => U;
33
+ /**
34
+ * Same as `array[index]`, but adds `undefined` to the return type. Maybe some
35
+ * fine day we will enable `noUncheckedIndexedAccess` in all our projects. 🤞
36
+ */
37
+ export declare const typeSafeIndex: <T>(array: T[], index: number) => T | undefined;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.dangerousUncheckedTypeCast = exports.exhaustiveTypeException = exports.exhaustiveTypeCheck = exports.isInArray = void 0;
3
+ exports.typeSafeIndex = exports.dangerousUncheckedTypeCast = exports.exhaustiveTypeException = exports.exhaustiveTypeCheck = exports.isInArray = void 0;
4
4
  const isInArray = (value, array) => array.includes(value);
5
5
  exports.isInArray = isInArray;
6
6
  const exhaustiveTypeCheck = (_) => void _;
@@ -30,3 +30,9 @@ exports.exhaustiveTypeException = exhaustiveTypeException;
30
30
  */
31
31
  const dangerousUncheckedTypeCast = (value) => value;
32
32
  exports.dangerousUncheckedTypeCast = dangerousUncheckedTypeCast;
33
+ /**
34
+ * Same as `array[index]`, but adds `undefined` to the return type. Maybe some
35
+ * fine day we will enable `noUncheckedIndexedAccess` in all our projects. 🤞
36
+ */
37
+ const typeSafeIndex = (array, index) => array[index];
38
+ exports.typeSafeIndex = typeSafeIndex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hevy-shared",
3
- "version": "1.0.698",
3
+ "version": "1.0.700",
4
4
  "description": "",
5
5
  "main": "built/index.js",
6
6
  "types": "built/index.d.ts",
@@ -35,6 +35,7 @@
35
35
  "typescript": "5.8.2"
36
36
  },
37
37
  "dependencies": {
38
+ "fuse.js": "^7.0.0",
38
39
  "lodash": "^4.17.21"
39
40
  }
40
41
  }