• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CBIIT / crdc-datahub-ui / 15497092546

06 Jun 2025 06:19PM UTC coverage: 65.179% (+2.5%) from 62.708%
15497092546

push

github

web-flow
Merge pull request #726 from CBIIT/CRDCDH-2817

CRDCDH-2817 Vite/Vitest Migration & Upgrade dependencies

3529 of 3882 branches covered (90.91%)

Branch coverage included in aggregate %.

167 of 224 new or added lines in 82 files covered. (74.55%)

7620 existing lines in 126 files now uncovered.

22012 of 35304 relevant lines covered (62.35%)

101.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.87
/src/utils/stringUtils.ts
1
import { Logger } from "./logger";
1✔
2

3
/**
4
 * Capitalizes the first letter of a given string.
5
 * If the input string is empty, it returns an empty string.
6
 *
7
 * @param {string} str - The string to capitalize.
8
 * @returns {string} - The capitalized string or an empty string if the input is empty.
9
 */
10
export const capitalizeFirstLetter = (str: string): string =>
1✔
11
  str ? str[0].toUpperCase() + str.slice(1) : "";
1,015✔
12

13
/**
14
 * Capitalizes the first letter of each word in a given string.
15
 *
16
 * @see Utilizes {@link capitalizeFirstLetter} to capitalize each word.
17
 * @param str - The string to capitalize.
18
 * @returns The capitalized string.
19
 */
20
export const titleCase = (str: string): string => {
1✔
21
  if (typeof str !== "string") {
898✔
22
    return "";
111✔
23
  }
111✔
24

25
  return str
787✔
26
    .toLowerCase()
787✔
27
    .split(" ")
787✔
28
    .map((word) => capitalizeFirstLetter(word))
787✔
29
    .join(" ");
787✔
30
};
787✔
31

32
/**
33
 * Function to add a space between a number and a letter in a string.
34
 * @param input - The input string to be processed. It should be a string where a number is directly followed by a letter.
35
 * @returns The processed string with a space between the number and the letter. If the input string does not match the required pattern, the function will return the original string.
36
 */
37
export const addSpace = (input: string): string => {
1✔
38
  // Regular expression to match a pattern where a number is directly followed by a letter
39
  const regex = /(\d+)([a-zA-Z]+)/g;
×
40

41
  if (!regex.test(input)) {
×
42
    return input;
×
UNCOV
43
  }
×
44

45
  // Replace the matched pattern with the same pattern but with a space in between
46
  return input.replace(regex, "$1 $2");
×
UNCOV
47
};
×
48

49
/**
50
 * Format a phone number string to (###) ###-####
51
 *
52
 * @param phoneNumber input phone number string
53
 * @returns formatted phone number or original phoneNumber string if invalid
54
 */
55
export const formatPhoneNumber = (phoneNumber: string): string => {
1✔
56
  // Remove all non-digits from the string
57
  const cleanNumber = phoneNumber.replace(/\D/g, "");
×
58

59
  // Ensure we have exactly 10 digits for a valid US phone number
60
  if (cleanNumber.length !== 10) {
×
61
    // If we don't, return the original string
62
    return phoneNumber;
×
UNCOV
63
  }
×
64

65
  // Use a regex to insert the formatting elements
66
  const formatted = cleanNumber.replace(/(\d{3})(\d{3})(\d{4})/, "($1)$2-$3");
×
67

68
  return formatted;
×
UNCOV
69
};
×
70

71
/**
72
 * Filters out all characters from a string except for alphanumeric characters.
73
 * Can be customized to allow additional characters
74
 *
75
 * @param {string} input The input string to be filtered
76
 * @param {string} [extraChars=""] Additional characters to allow in the filtered string
77
 * @returns {string} The filtered string containing only alphanumeric characters and any
78
 * additional allowed characters. Returns formatted string, otherwise if no value is passed,
79
 * then it returns an empty string
80
 */
81
export const filterAlphaNumeric = (input: string, extraChars = ""): string => {
1✔
82
  // The base regex matches alphanumeric characters.
83
  // We add the `extraChars` by splitting it into individual characters, escaping each one, and then joining with "|".
84
  // This ensures characters with special meaning in regex (like ".") are treated as literal characters.
85
  const pattern = new RegExp(
47✔
86
    `[^a-zA-Z0-9${extraChars
47✔
87
      .split("")
47✔
88
      .map((char) => `\\${char}`)
47✔
89
      .join("|")}]`,
47✔
90
    "g"
47✔
91
  );
47✔
92

93
  // We replace characters that don't match the allowed set with an empty string.
94
  return input?.replace(pattern, "") || "";
47✔
95
};
47✔
96

97
/**
98
 * Filters a string to allow only positive integers.
99
 *
100
 * @param {string} input The input string to be filtered
101
 * @returns {string} A string containing only positive integers, otherwise an empty string
102
 */
103
export const filterPositiveIntegerString = (input: string): string => {
1✔
104
  if (!input) {
14✔
105
    return "";
3✔
106
  }
3✔
107

108
  const nonIntegerPattern = /[^0-9]/g;
11✔
109
  const filtered = input.replace(nonIntegerPattern, "");
11✔
110

111
  // Remove leading zeros using a regular expression
112
  const noLeadingZeros = filtered.replace(/^0+/, "");
11✔
113

114
  return noLeadingZeros || "";
14✔
115
};
14✔
116

117
/**
118
 * Compares two string values for sorting in an array. Non-empty strings are sorted
119
 * alphabetically, and `null` or empty strings are placed at the beginning.
120
 *
121
 * @param {string | null} a - The first string to compare.
122
 * @param {string | null} b - The second string to compare.
123
 * @returns {number} - A negative number if `a` should come before `b`, a positive number
124
 * if `a` should come after `b`, or zero if they are considered equal for sorting purposes.
125
 */
126
export const compareStrings = (a: string | null, b: string | null): number => {
1✔
127
  if (a === b) return 0;
24✔
128
  if (!a || a === "") return -1;
24✔
129
  if (!b || b === "") return 1;
24✔
130

131
  return a.localeCompare(b);
17✔
132
};
17✔
133

134
/**
135
 * Moves the specified key element to the front of the array.
136
 * If the key element does not exist in the array, the original array is returned.
137
 *
138
 * @param {string[]} array - The array of strings to be reordered.
139
 * @param {string} keyElement - The key element to move to the front of the array.
140
 * @returns {string[]} A new array with the key element moved to the front if present.
141
 */
142
export const moveToFrontOfArray = (array: string[], keyElement: string): string[] => {
1✔
143
  if (!array?.length) {
24✔
144
    return [];
2✔
145
  }
2✔
146
  if (!keyElement?.length) {
24✔
147
    return array;
2✔
148
  }
2✔
149

150
  const index = array.indexOf(keyElement);
20✔
151

152
  // Return original array if keyElement is not found or already at the first position
153
  if (index <= 0) {
24✔
154
    return array;
19✔
155
  }
19✔
156

157
  const newArray = [...array];
1✔
158
  newArray.splice(index, 1);
1✔
159
  newArray.unshift(keyElement);
1✔
160

161
  return newArray;
1✔
162
};
1✔
163

164
/**
165
 * Rearranges the order of specified keys in an array and appends the remaining keys to the end.
166
 *
167
 * @param {string[]} keysArray - The array of keys to be processed.
168
 * @param {string[]} keyOrder - An array specifying the desired order of keys.
169
 * @returns {string[]} A new array with keys in the specified order, followed by the remaining keys.
170
 */
171
export const rearrangeKeys = (keysArray: string[], keyOrder: string[]): string[] => {
1✔
172
  if (!Array.isArray(keysArray) || !Array.isArray(keyOrder)) {
49✔
173
    return keysArray || [];
4✔
174
  }
4✔
175

176
  const orderedKeys = keyOrder.filter((key) => keysArray.includes(key));
45✔
177
  const remainingKeys = keysArray.filter((key) => !orderedKeys.includes(key));
45✔
178

179
  return [...orderedKeys, ...remainingKeys];
45✔
180
};
45✔
181

182
/**
183
 * Checks if the length of a given string is between the specified minimum and maximum values.
184
 *
185
 * @param str - The string to check. Can be null or undefined.
186
 * @param minLength - The minimum allowable length of the string.
187
 * @param maxLength - The maximum allowable length of the string.
188
 * @returns True if the string's length is strictly between minLength and maxLength, exclusive; otherwise, false.
189
 */
190
export const isStringLengthBetween = (
1✔
191
  str: string,
28✔
192
  minLength: number,
28✔
193
  maxLength: number
28✔
194
): boolean => {
28✔
195
  if (typeof str !== "string") {
28✔
196
    return false;
3✔
197
  }
3✔
198

199
  return str?.length > minLength && str?.length < maxLength;
28✔
200
};
28✔
201

202
/**
203
 * Extracts the major and minor version numbers from a version string.
204
 *
205
 * @param {string} version - The version string to parse.
206
 * @returns {string} A string representing the major and minor version numbers.
207
 * Otherwise, an empty string.
208
 */
209
export const extractVersion = (version: string): string => {
1✔
210
  if (!version || typeof version !== "string") {
123✔
211
    Logger.error(`extractVersion: Invalid version value provided.`, version);
20✔
212
    return "";
20✔
213
  }
20✔
214

215
  const firstPeriodIndex: number = version.indexOf(".");
103✔
216
  if (firstPeriodIndex === -1) {
107✔
217
    Logger.error(
1✔
218
      `extractVersion: Invalid version string: "${version}". Expected at least one period to separate major and minor versions.`
1✔
219
    );
1✔
220
    return "";
1✔
221
  }
1✔
222

223
  const majorPart: string = version.substring(0, firstPeriodIndex);
102✔
224
  const remainder: string = version.substring(firstPeriodIndex + 1);
102✔
225

226
  const majorMatch: RegExpMatchArray | null = majorPart.match(/\d+/);
102✔
227
  if (!majorMatch) {
107✔
228
    Logger.error(`extractVersion: Invalid major version in string: "${version}"`);
2✔
229
    return "";
2✔
230
  }
2✔
231
  const major: string = majorMatch[0];
100✔
232

233
  const minorMatch: RegExpMatchArray | null = remainder.match(/^\d+/);
100✔
234
  if (!minorMatch) {
107✔
235
    Logger.error(`extractVersion: Invalid minor version in string: "${version}"`);
1✔
236
    return "";
1✔
237
  }
1✔
238
  const minor: string = minorMatch[0];
99✔
239

240
  return `${major}.${minor}`;
99✔
241
};
99✔
242

243
/**
244
 * A utility function to safely convert an unknown value to a string.
245
 *
246
 * Notes on handling:
247
 * - Objects and arrays are converted to JSON strings.
248
 * - Dates are converted to ISO strings.
249
 * - Other types are converted using their `toString` method if available.
250
 * - `null` and `undefined` are converted to empty strings.
251
 *
252
 * @note This handles a wider range of types than `lodash.toString`
253
 * @param value The unknown value to coerce
254
 * @returns The coerced string or empty if unhandled type
255
 */
256
export const coerceToString = (value: unknown): string => {
1✔
257
  // Handle null or undefined values
258
  if (value === undefined || value === null) {
794✔
259
    return "";
672✔
260
  }
672✔
261

262
  // Short-circuit for strings
263
  if (typeof value === "string") {
131✔
264
    return value;
55✔
265
  }
55✔
266

267
  // Handle valid date objects
268
  if (value instanceof Date && isFinite(+value)) {
794✔
269
    return value.toISOString();
4✔
270
  }
4✔
271

272
  // Handle arrays or objects by converting to JSON
273
  if (typeof value === "object" && !(value instanceof Date)) {
794✔
274
    return JSON.stringify(value);
10✔
275
  }
10✔
276

277
  // Handle other native types with toString method
278
  if (
53✔
279
    ["boolean", "number", "bigint"].includes(typeof value) &&
53✔
280
    typeof value?.toString === "function"
49✔
281
  ) {
794✔
282
    return value.toString();
49✔
283
  }
49✔
284

285
  Logger.error(`coerceToString: Unhandled type received '${typeof value}'`);
4✔
286
  return "";
4✔
287
};
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc