Coverage for src / toolbox_python / strings.py: 100%

26 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-02 22:56 +0000

1# ============================================================================ # 

2# # 

3# Title : Strings # 

4# Purpose : Manipulate and check strings. # 

5# # 

6# ============================================================================ # 

7 

8 

9# ---------------------------------------------------------------------------- # 

10# # 

11# Overview #### 

12# # 

13# ---------------------------------------------------------------------------- # 

14 

15 

16# ---------------------------------------------------------------------------- # 

17# Description #### 

18# ---------------------------------------------------------------------------- # 

19 

20 

21""" 

22!!! note "Summary" 

23 The `strings` module is for manipulating and checking certain string objects. 

24""" 

25 

26 

27# ---------------------------------------------------------------------------- # 

28# # 

29# Setup #### 

30# # 

31# ---------------------------------------------------------------------------- # 

32 

33 

34# ---------------------------------------------------------------------------- # 

35# Imports #### 

36# ---------------------------------------------------------------------------- # 

37 

38 

39# ## Python StdLib Imports ---- 

40import re 

41import string 

42from typing import Any, Union, overload 

43 

44# ## Python Third Party Imports ---- 

45from typeguard import typechecked 

46 

47# ## Local First Party Imports ---- 

48from toolbox_python.checkers import is_type 

49 

50 

51# ---------------------------------------------------------------------------- # 

52# Exports #### 

53# ---------------------------------------------------------------------------- # 

54 

55 

56__all__: list[str] = [ 

57 "str_replace", 

58 "str_contains", 

59 "str_contains_any", 

60 "str_contains_all", 

61 "str_separate_number_chars", 

62 "str_to_list", 

63] 

64 

65 

66# ---------------------------------------------------------------------------- # 

67# # 

68# Functions #### 

69# # 

70# ---------------------------------------------------------------------------- # 

71 

72 

73@typechecked 

74def str_replace( 

75 old_string: str, 

76 replace_chars: str = string.punctuation + string.whitespace, 

77 replace_with: str = "", 

78) -> str: 

79 """ 

80 !!! note "Summary" 

81 Replace the characters with a given string. 

82 

83 ???+ abstract "Details" 

84 Similar to the Python `#!py str.replace()` method, but provides more customisation through the use of the [`re`](https://docs.python.org/3/library/re.html) package. 

85 

86 Params: 

87 old_string (str): 

88 The old string to be replaced. 

89 replace_chars (str, optional): 

90 The characters that need replacing.<br> 

91 Defaults to `#!py string.punctuation + string.whitespace`. 

92 replace_with (str, optional): 

93 The value to replace the characters with.<br> 

94 Defaults to `""`. 

95 

96 Raises: 

97 (TypeCheckError): 

98 If any of the inputs parsed to the parameters of this function are not the correct type. Uses the [`@typeguard.typechecked`](https://typeguard.readthedocs.io/en/stable/api.html#typeguard.typechecked) decorator. 

99 

100 Returns: 

101 (str): 

102 The new formatted string. 

103 

104 ???+ example "Examples" 

105 

106 ```pycon {.py .python linenums="1" title="Set up"} 

107 >>> from toolbox_python.strings import str_replace 

108 >>> long_string = "This long string" 

109 >>> complex_sentence = "Because my pizza was cold, I put it in the microwave." 

110 ``` 

111 

112 ```pycon {.py .python linenums="1" title="Example 1: Replace all spaces (` `) with underscore (`_`)"} 

113 >>> print(str_replace(long_string, " ", "_")) 

114 ``` 

115 <div class="result" markdown> 

116 ```{.sh .shell title="Terminal"} 

117 "This_long_string" 

118 ``` 

119 !!! success "Conclusion: Successful conversion." 

120 </div> 

121 

122 ```pycon {.py .python linenums="1" title="Example 2: Remove all punctuation and white space"} 

123 >>> print(str_replace(complex_sentence)) 

124 ``` 

125 <div class="result" markdown> 

126 ```{.sh .shell title="Terminal"} 

127 "BecausemylunchwascoldIputitinthemicrowave" 

128 ``` 

129 !!! success "Conclusion: Successful conversion." 

130 </div> 

131 

132 ```pycon {.py .python linenums="1" title="Example 3: Invalid `old_string` input"} 

133 >>> print(str_replace(123)) 

134 ``` 

135 <div class="result" markdown> 

136 ```{.sh .shell title="Terminal"} 

137 TypeError: ... 

138 ``` 

139 !!! failure "Conclusion: Invalid input." 

140 !!! observation "Note: The same error will occur if `replace_chars` or `replace_with` are not of type `str`." 

141 </div> 

142 

143 ??? success "Credit" 

144 Full credit goes to:<br> 

145 https://stackoverflow.com/questions/23996118/replace-special-characters-in-a-string-python#answer-23996414 

146 

147 ??? tip "See Also" 

148 - [`re`](https://docs.python.org/3/library/re.html) 

149 """ 

150 chars: str = re.escape(replace_chars) 

151 return re.sub(rf"[{chars}]", replace_with, old_string) 

152 

153 

154@typechecked 

155def str_contains(check_string: str, sub_string: str) -> bool: 

156 """ 

157 !!! note "Summary" 

158 Check whether one string contains another string. 

159 

160 ???+ abstract "Details" 

161 This is a super simple one-line function. 

162 

163 ```py linenums="1" title="Example" 

164 return True if sub_string in check_string else False 

165 ``` 

166 

167 Params: 

168 check_string (str): 

169 The main string to check. 

170 sub_string (str): 

171 The substring to check. 

172 

173 Raises: 

174 (TypeCheckError): 

175 If any of the inputs parsed to the parameters of this function are not the correct type. Uses the [`@typeguard.typechecked`](https://typeguard.readthedocs.io/en/stable/api.html#typeguard.typechecked) decorator. 

176 

177 Returns: 

178 (bool): 

179 `#!py True` if `#!py sub_string` in `#!py check_string` 

180 

181 ???+ example "Examples" 

182 

183 ```pycon {.py .python linenums="1" title="Set up"} 

184 >>> from toolbox_python.strings import str_contains 

185 >>> long_string = "This long string" 

186 ``` 

187 

188 ```pycon {.py .python linenums="1" title="Example 1: String is contained"} 

189 >>> print(str_contains(long_string, "long")) 

190 ``` 

191 <div class="result" markdown> 

192 ```{.sh .shell title="Terminal"} 

193 True 

194 ``` 

195 !!! success "Conclusion: `#!py long_string` contains `#!py "long"`." 

196 </div> 

197 

198 ```pycon {.py .python linenums="1" title="Example 2: String is not contained"} 

199 >>> print(str_contains(long_string, "short")) 

200 ``` 

201 <div class="result" markdown> 

202 ```{.sh .shell title="Terminal"} 

203 False 

204 ``` 

205 !!! success "Conclusion: `#!py long_string` does not contain `#!py "short"`." 

206 </div> 

207 

208 ```pycon {.py .python linenums="1" title="Example 3: Invalid `check_string` input"} 

209 >>> print(str_contains(123, "short")) 

210 ``` 

211 <div class="result" markdown> 

212 ```{.sh .shell title="Terminal"} 

213 TypeError: ... 

214 ``` 

215 !!! failure "Conclusion: Invalid input." 

216 !!! observation "Note: The same error will occur if `sub_string` is not of type `str`." 

217 </div> 

218 

219 ??? tip "See Also" 

220 - [`str_contains_any()`][toolbox_python.strings.str_contains_any] 

221 - [`str_contains_all()`][toolbox_python.strings.str_contains_all] 

222 """ 

223 return sub_string in check_string 

224 

225 

226@typechecked 

227def str_contains_any( 

228 check_string: str, 

229 sub_strings: Union[list[str], tuple[str, ...]], 

230) -> bool: 

231 """ 

232 !!! note "Summary" 

233 Check whether any one of a number of strings are contained within a main string. 

234 

235 Params: 

236 check_string (str): 

237 The main string to check. 

238 sub_strings (Union[list[str], tuple[str, ...]]): 

239 The collection of substrings to check. 

240 

241 Raises: 

242 (TypeCheckError): 

243 If any of the inputs parsed to the parameters of this function are not the correct type. Uses the [`@typeguard.typechecked`](https://typeguard.readthedocs.io/en/stable/api.html#typeguard.typechecked) decorator. 

244 

245 Returns: 

246 (bool): 

247 `#!py True` if `#!py any` of the strings in `#!py sub_strings` are contained within `#!py check_string`. 

248 

249 ???+ example "Examples" 

250 

251 ```pycon {.py .python linenums="1" title="Set up"} 

252 >>> from toolbox_python.strings import str_contains_any 

253 >>> long_string = "This long string" 

254 ``` 

255 

256 ```pycon {.py .python linenums="1" title="Example 1: Contains any"} 

257 >>> print(str_contains_any(long_string, ["long", "short"])) 

258 ``` 

259 <div class="result" markdown> 

260 ```{.sh .shell title="Terminal"} 

261 True 

262 ``` 

263 !!! success "Conclusion: `#!py long_string` contains either `#!py "long"` or `#!py "short"`." 

264 </div> 

265 

266 ```pycon {.py .python linenums="1" title="Example 2: Contains none"} 

267 >>> print(str_contains_any(long_string, ["this", "that"])) 

268 ``` 

269 <div class="result" markdown> 

270 ```{.sh .shell title="Terminal"} 

271 False 

272 ``` 

273 !!! success "Conclusion: `#!py long_string` contains neither `#!py "this"` nor `#!py "that"`." 

274 </div> 

275 

276 ```pycon {.py .python linenums="1" title="Example 3: Invalid `check_string` input"} 

277 >>> print(str_contains_any(123, ["short", "long"])) 

278 ``` 

279 <div class="result" markdown> 

280 ```{.sh .shell title="Terminal"} 

281 TypeError: ... 

282 ``` 

283 !!! failure "Conclusion: Invalid input." 

284 !!! observation "Note: The same error will occur if any of the elements in `sub_strings` are not of type `str`." 

285 </div> 

286 

287 ??? tip "See Also" 

288 - [`str_contains()`][toolbox_python.strings.str_contains] 

289 - [`str_contains_all()`][toolbox_python.strings.str_contains_all] 

290 """ 

291 return any( 

292 str_contains( 

293 check_string=check_string, 

294 sub_string=sub_string, 

295 ) 

296 for sub_string in sub_strings 

297 ) 

298 

299 

300@typechecked 

301def str_contains_all( 

302 check_string: str, 

303 sub_strings: Union[list[str], tuple[str, ...]], 

304) -> bool: 

305 """ 

306 !!! note "Summary" 

307 Check to ensure that all sub-strings are contained within a main string. 

308 

309 Params: 

310 check_string (str): 

311 The main string to check. 

312 sub_strings (Union[list[str], tuple[str, ...]]): 

313 The collection of substrings to check. 

314 

315 Raises: 

316 (TypeCheckError): 

317 If any of the inputs parsed to the parameters of this function are not the correct type. Uses the [`@typeguard.typechecked`](https://typeguard.readthedocs.io/en/stable/api.html#typeguard.typechecked) decorator. 

318 

319 Returns: 

320 (bool): 

321 `#!py True` if `#!py all` of the strings in `#!py sub_strings` are contained within `#!py check_string`. 

322 

323 ???+ example "Examples" 

324 

325 ```pycon {.py .python linenums="1" title="Set up"} 

326 >>> from toolbox_python.strings import str_contains_all 

327 >>> long_string = "This long string" 

328 ``` 

329 

330 ```pycon {.py .python linenums="1" title="Example 1: Contains all"} 

331 >>> print(str_contains_all(long_string, ["long", "string"])) 

332 ``` 

333 <div class="result" markdown> 

334 ```{.sh .shell title="Terminal"} 

335 True 

336 ``` 

337 !!! success "Conclusion: `#!py long_string` contains both `#!py "long"` and `#!py "string"`." 

338 </div> 

339 

340 ```pycon {.py .python linenums="1" title="Example 2: Contains some"} 

341 >>> print(str_contains_all(long_string, ["long", "something"])) 

342 ``` 

343 <div class="result" markdown> 

344 ```{.sh .shell title="Terminal"} 

345 False 

346 ``` 

347 !!! failure "Conclusion: `#!py long_string` contains `#!py "long"` but not `#!py "something"`." 

348 </div> 

349 

350 ```pycon {.py .python linenums="1" title="Example 3: Contains none"} 

351 >>> print(str_contains_all(long_string, ["this", "that"])) 

352 ``` 

353 <div class="result" markdown> 

354 ```{.sh .shell title="Terminal"} 

355 False 

356 ``` 

357 !!! failure "Conclusion: `#!py long_string` contains neither `#!py "this"` nor `#!py "that"`." 

358 </div> 

359 

360 ```pycon {.py .python linenums="1" title="Example 4: Invalid `check_string` input"} 

361 >>> print(str_contains_all(123, ["short", "long"])) 

362 ``` 

363 <div class="result" markdown> 

364 ```{.sh .shell title="Terminal"} 

365 TypeError: ... 

366 ``` 

367 !!! failure "Conclusion: Invalid input." 

368 !!! observation "Note: The same error will occur if any of the elements in `sub_strings` are not of type `str`." 

369 </div> 

370 

371 ??? tip "See Also" 

372 - [`str_contains()`][toolbox_python.strings.str_contains] 

373 - [`str_contains_any()`][toolbox_python.strings.str_contains_any] 

374 """ 

375 return all( 

376 str_contains( 

377 check_string=check_string, 

378 sub_string=sub_string, 

379 ) 

380 for sub_string in sub_strings 

381 ) 

382 

383 

384@typechecked 

385def str_separate_number_chars(text: str) -> list[str]: 

386 """ 

387 !!! note "Summary" 

388 Take in a string that contains both numbers and letters, and output a list of strings, separated to have each element containing either entirely number or entirely letters. 

389 

390 ???+ abstract "Details" 

391 Uses regex ([`re.split()`](https://docs.python.org/3/library/re.html#re.split)) to perform the actual splitting.<br> 

392 Note, it _will_ preserve special characters & punctuation, but it _will not_ preserve whitespaces. 

393 

394 Params: 

395 text (str): 

396 The string to split. 

397 

398 Raises: 

399 (TypeCheckError): 

400 If any of the inputs parsed to the parameters of this function are not the correct type. Uses the [`@typeguard.typechecked`](https://typeguard.readthedocs.io/en/stable/api.html#typeguard.typechecked) decorator. 

401 

402 Returns: 

403 (list[str]): 

404 The updated list, with each element of the list containing either entirely characters or entirely numbers. 

405 

406 ???+ example "Examples" 

407 

408 ```pycon {.py .python linenums="1" title="Set up"} 

409 >>> from toolbox_python.strings import str_contains_all 

410 >>> simple_string = "-12.1grams" 

411 >>> complex_string = "abcd2343 abw34324 abc3243-23A 123" 

412 ``` 

413 

414 ```pycon {.py .python linenums="1" title="Example 1: Simple split"} 

415 >>> print(str_separate_number_chars(simple_string)) 

416 ``` 

417 <div class="result" markdown> 

418 ```{.sh .shell title="Terminal"} 

419 ["-12.1", "grams"] 

420 ``` 

421 !!! success "Conclusion: Successful split." 

422 </div> 

423 

424 ```pycon {.py .python linenums="1" title="Example 2: Complex split"} 

425 >>> print(str_separate_number_chars(complex_string)) 

426 ``` 

427 <div class="result" markdown> 

428 ```{.sh .shell title="Terminal"} 

429 [ 

430 "abcd", 

431 "2343", 

432 "abw", 

433 "34324", 

434 "abc", 

435 "3243", 

436 "-23", 

437 "A", 

438 "123", 

439 ] 

440 ``` 

441 !!! success "Conclusion: Successful split." 

442 </div> 

443 

444 ```pycon {.py .python linenums="1" title="Example 3: `text` does not contain any numbers"} 

445 >>> print(str_separate_number_chars("abcd")) 

446 ``` 

447 <div class="result" markdown> 

448 ```{.sh .shell title="Terminal"} 

449 ["abcd"] 

450 ``` 

451 !!! success "Conclusion: No numbers in `#!py text`, so returns a single-element long list." 

452 </div> 

453 

454 ```pycon {.py .python linenums="1" title="Example 4: Invalid `text` input"} 

455 >>> print(str_separate_number_chars(123)) 

456 ``` 

457 <div class="result" markdown> 

458 ```{.sh .shell title="Terminal"} 

459 TypeError: ... 

460 ``` 

461 !!! failure "Conclusion: Invalid input." 

462 </div> 

463 

464 ??? success "Credit" 

465 Full credit goes to:<br> 

466 https://stackoverflow.com/questions/3340081/product-code-looks-like-abcd2343-how-to-split-by-letters-and-numbers#answer-63362709. 

467 

468 ??? tip "See Also" 

469 - [`re`](https://docs.python.org/3/library/re.html) 

470 """ 

471 res = re.split(r"([-+]?\d+\.\d+)|([-+]?\d+)", text.strip()) 

472 return [r.strip() for r in res if r is not None and r.strip() != ""] 

473 

474 

475@overload 

476@typechecked 

477def str_to_list(obj: str) -> list[str]: ... 

478@overload 

479@typechecked 

480def str_to_list(obj: Any) -> Any: ... 

481@typechecked 

482def str_to_list(obj: Any) -> Union[list[str], Any]: 

483 """ 

484 !!! note "Summary" 

485 Convert a string to a list containing that string as the only element. 

486 

487 ???+ abstract "Details" 

488 This function is useful when you want to ensure that a string is treated as a list, even if it is a single string. If the input is already a list, it will return it unchanged. 

489 

490 Params: 

491 obj (Any): 

492 The object to convert to a list if it is a string. 

493 

494 Raises: 

495 (TypeCheckError): 

496 If `obj` is not a string or a list. 

497 

498 Returns: 

499 (Union[list[str], Any]): 

500 If `obj` is a string, returns a list containing that string as the only element. If `obj` is not a string, returns it unchanged. 

501 

502 ???+ example "Examples" 

503 ```pycon {.py .python linenums="1" title="Set up"} 

504 >>> from toolbox_python.strings import str_to_list 

505 ``` 

506 

507 ```pycon {.py .python linenums="1" title="Example 1: Convert string to list"} 

508 >>> print(str_to_list("hello")) 

509 ``` 

510 <div class="result" markdown> 

511 ```{.sh .shell title="Terminal"} 

512 ["hello"] 

513 ``` 

514 !!! success "Conclusion: Successfully converted string to list." 

515 </div> 

516 

517 ```pycon {.py .python linenums="1" title="Example 2: Input is already a list"} 

518 >>> print(str_to_list(["hello", "world"])) 

519 ``` 

520 <div class="result" markdown> 

521 ```{.sh .shell title="Terminal"} 

522 ["hello", "world"] 

523 ``` 

524 !!! success "Conclusion: Input was already a list, so returned unchanged." 

525 </div> 

526 """ 

527 return [obj] if is_type(obj, str) else obj