orionis 0.716.0__py3-none-any.whl → 0.718.0__py3-none-any.whl
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.
- orionis/metadata/framework.py +1 -1
- orionis/support/types/stringable.py +3100 -0
- {orionis-0.716.0.dist-info → orionis-0.718.0.dist-info}/METADATA +1 -1
- {orionis-0.716.0.dist-info → orionis-0.718.0.dist-info}/RECORD +7 -6
- {orionis-0.716.0.dist-info → orionis-0.718.0.dist-info}/WHEEL +0 -0
- {orionis-0.716.0.dist-info → orionis-0.718.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.716.0.dist-info → orionis-0.718.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,3100 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import base64
|
|
3
|
+
import json
|
|
4
|
+
import hashlib
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import uuid
|
|
7
|
+
import os
|
|
8
|
+
import html
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Callable, Iterable, Optional, Union, List, Dict
|
|
11
|
+
import unicodedata
|
|
12
|
+
|
|
13
|
+
class Stringable(str):
|
|
14
|
+
|
|
15
|
+
def after(self, search: str) -> "Stringable":
|
|
16
|
+
"""
|
|
17
|
+
Return the substring after the first occurrence of a given value.
|
|
18
|
+
|
|
19
|
+
Searches for the first occurrence of the specified substring and returns everything that comes after it. If the substring is not found, returns the original string unchanged.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
search : str
|
|
24
|
+
Substring to search for within the current string.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
Stringable
|
|
29
|
+
New Stringable instance containing the substring after the first occurrence of the search string, or the original string if not found.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Find the index of the first occurrence of the search string
|
|
33
|
+
idx = self.find(search)
|
|
34
|
+
|
|
35
|
+
# If found, return substring after the search string
|
|
36
|
+
# Otherwise, return the original string
|
|
37
|
+
return Stringable(self[idx + len(search):]) if idx != -1 else Stringable(self)
|
|
38
|
+
|
|
39
|
+
def afterLast(self, search: str) -> "Stringable":
|
|
40
|
+
"""
|
|
41
|
+
Return the substring after the last occurrence of a given value.
|
|
42
|
+
|
|
43
|
+
Searches for the last occurrence of the specified substring and returns everything that comes after it. If the substring is not found, returns the original string unchanged.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
search : str
|
|
48
|
+
Substring to search for within the current string.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
Stringable
|
|
53
|
+
New Stringable instance containing the substring after the last occurrence of the search string, or the original string if not found.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# Find the index of the last occurrence of the search string
|
|
57
|
+
idx = self.rfind(search)
|
|
58
|
+
|
|
59
|
+
# If found, return substring after the search string
|
|
60
|
+
# Otherwise, return the original string
|
|
61
|
+
return Stringable(self[idx + len(search):]) if idx != -1 else Stringable(self)
|
|
62
|
+
|
|
63
|
+
def append(self, *values: str) -> "Stringable":
|
|
64
|
+
"""
|
|
65
|
+
Append one or more values to the end of the string.
|
|
66
|
+
|
|
67
|
+
Concatenates the provided string values to the end of the current string, returning a new Stringable instance.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
*values : str
|
|
72
|
+
One or more string values to append.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
Stringable
|
|
77
|
+
New Stringable instance with all provided values appended to the end.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# Join all provided values and append them to the current string
|
|
81
|
+
return Stringable(self + "".join(values))
|
|
82
|
+
|
|
83
|
+
def newLine(self, count: int = 1) -> "Stringable":
|
|
84
|
+
"""
|
|
85
|
+
Append one or more newline characters to the string.
|
|
86
|
+
|
|
87
|
+
Appends the specified number of newline characters (\n) to the end of the current string.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
count : int, optional
|
|
92
|
+
Number of newline characters to append. Default is 1.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
Stringable
|
|
97
|
+
New Stringable instance with the specified number of newline characters appended.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
# Append the specified number of newline characters to the current string
|
|
101
|
+
return Stringable(str(self) + "\n" * count)
|
|
102
|
+
|
|
103
|
+
def before(self, search: str) -> "Stringable":
|
|
104
|
+
"""
|
|
105
|
+
Return the substring before the first occurrence of a given value.
|
|
106
|
+
|
|
107
|
+
Searches for the first occurrence of the specified substring and returns everything before it. If the substring is not found, returns the original string unchanged.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
search : str
|
|
112
|
+
Substring to search for within the current string.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
Stringable
|
|
117
|
+
New Stringable instance containing the substring before the first occurrence of the search string, or the original string if not found.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
# Find the index of the first occurrence of the search string
|
|
121
|
+
idx = self.find(search)
|
|
122
|
+
|
|
123
|
+
# If found, return substring before the search string
|
|
124
|
+
# Otherwise, return the original string
|
|
125
|
+
return Stringable(self[:idx]) if idx != -1 else Stringable(self)
|
|
126
|
+
|
|
127
|
+
def beforeLast(self, search: str) -> "Stringable":
|
|
128
|
+
"""
|
|
129
|
+
Return the substring before the last occurrence of a given value.
|
|
130
|
+
|
|
131
|
+
Searches for the last occurrence of the specified substring and returns everything before it. If the substring is not found, returns the original string unchanged.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
search : str
|
|
136
|
+
Substring to search for within the current string.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
Stringable
|
|
141
|
+
New Stringable instance containing the substring before the last occurrence of the search string, or the original string if not found.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
# Find the index of the last occurrence of the search string
|
|
145
|
+
idx = self.rfind(search)
|
|
146
|
+
|
|
147
|
+
# If found, return substring before the search string
|
|
148
|
+
# Otherwise, return the original string
|
|
149
|
+
return Stringable(self[:idx]) if idx != -1 else Stringable(self)
|
|
150
|
+
|
|
151
|
+
def contains(self, needles: Union[str, Iterable[str]], ignore_case: bool = False) -> bool:
|
|
152
|
+
"""
|
|
153
|
+
Check if the string contains any of the given values.
|
|
154
|
+
|
|
155
|
+
Determines whether the string contains any of the specified needle values. The search can be performed case-sensitively or case-insensitively.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
needles : str or Iterable[str]
|
|
160
|
+
Value or values to search for within the string.
|
|
161
|
+
ignore_case : bool, optional
|
|
162
|
+
If True, perform case-insensitive search. Default is False.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
bool
|
|
167
|
+
True if the string contains any of the needle values, False otherwise.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
# Normalize needles to a list for consistent processing
|
|
171
|
+
if isinstance(needles, str):
|
|
172
|
+
needles = [needles]
|
|
173
|
+
|
|
174
|
+
# Convert to lowercase for case-insensitive comparison if requested
|
|
175
|
+
s = str(self).lower() if ignore_case else str(self)
|
|
176
|
+
|
|
177
|
+
# Check if any needle is found in the string
|
|
178
|
+
return any((needle.lower() if ignore_case else needle) in s for needle in needles)
|
|
179
|
+
|
|
180
|
+
def endsWith(self, needles: Union[str, Iterable[str]]) -> bool:
|
|
181
|
+
"""
|
|
182
|
+
Check if the string ends with any of the given substrings.
|
|
183
|
+
|
|
184
|
+
Determines whether the string ends with any of the specified needle values.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
needles : str or Iterable[str]
|
|
189
|
+
Substring or substrings to check at the end of the string.
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
bool
|
|
194
|
+
True if the string ends with any of the needle values, False otherwise.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
# Normalize needles to a list for consistent processing
|
|
198
|
+
if isinstance(needles, str):
|
|
199
|
+
needles = [needles]
|
|
200
|
+
|
|
201
|
+
# Check if string ends with any of the provided needles
|
|
202
|
+
return any(str(self).endswith(needle) for needle in needles)
|
|
203
|
+
|
|
204
|
+
def exactly(self, value: Any) -> bool:
|
|
205
|
+
"""
|
|
206
|
+
Check if the string is exactly equal to a given value.
|
|
207
|
+
|
|
208
|
+
Performs a strict equality comparison between the string and the provided value after converting both to string representations.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
value : Any
|
|
213
|
+
Value to compare against the current string.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
bool
|
|
218
|
+
True if the string exactly matches the given value, False otherwise.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
# Convert both values to strings and compare for exact equality
|
|
222
|
+
return str(self) == str(value)
|
|
223
|
+
|
|
224
|
+
def isEmpty(self) -> bool:
|
|
225
|
+
"""
|
|
226
|
+
Check if the string is empty.
|
|
227
|
+
|
|
228
|
+
Determines if the string has zero length, meaning it contains no characters.
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
bool
|
|
233
|
+
True if the string is empty, False otherwise.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
# Return True if the string has zero length
|
|
237
|
+
return len(self) == 0
|
|
238
|
+
|
|
239
|
+
def isNotEmpty(self) -> bool:
|
|
240
|
+
"""
|
|
241
|
+
Check if the string is not empty.
|
|
242
|
+
|
|
243
|
+
Determines if the string has one or more characters, meaning it contains some content.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
bool
|
|
248
|
+
True if the string is not empty, False otherwise.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
# Return True if the string has one or more characters
|
|
252
|
+
return not self.isEmpty()
|
|
253
|
+
|
|
254
|
+
def lower(self) -> "Stringable":
|
|
255
|
+
"""
|
|
256
|
+
Convert the string to lowercase.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
Stringable
|
|
261
|
+
New Stringable instance with all characters converted to lowercase.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
# Convert all characters to lowercase using the built-in method
|
|
265
|
+
return Stringable(super().lower())
|
|
266
|
+
|
|
267
|
+
def upper(self) -> "Stringable":
|
|
268
|
+
"""
|
|
269
|
+
Convert the string to uppercase.
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
Stringable
|
|
274
|
+
New Stringable instance with all characters converted to uppercase.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
# Convert all characters to uppercase using the built-in method
|
|
278
|
+
return Stringable(super().upper())
|
|
279
|
+
|
|
280
|
+
def reverse(self) -> "Stringable":
|
|
281
|
+
"""
|
|
282
|
+
Reverse the string.
|
|
283
|
+
|
|
284
|
+
Returns
|
|
285
|
+
-------
|
|
286
|
+
Stringable
|
|
287
|
+
New Stringable instance with characters in reverse order.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
# Reverse the string using slicing
|
|
291
|
+
return Stringable(self[::-1])
|
|
292
|
+
|
|
293
|
+
def repeat(self, times: int) -> "Stringable":
|
|
294
|
+
"""
|
|
295
|
+
Repeat the string a specified number of times.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
times : int
|
|
300
|
+
Number of times to repeat the string.
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
Stringable
|
|
305
|
+
New Stringable instance with the string repeated the specified number of times.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
# Repeat the string using multiplication
|
|
309
|
+
return Stringable(self * times)
|
|
310
|
+
|
|
311
|
+
def replace(self, search: Union[str, Iterable[str]], replace: Union[str, Iterable[str]], case_sensitive: bool = True) -> "Stringable":
|
|
312
|
+
"""
|
|
313
|
+
Replace occurrences of specified substrings with corresponding replacements.
|
|
314
|
+
|
|
315
|
+
Replaces each substring in `search` with the corresponding value in `replace`. If `search` or `replace` is a single string, it is converted to a list for uniform processing. The replacement can be performed case-sensitively or case-insensitively.
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
----------
|
|
319
|
+
search : str or Iterable[str]
|
|
320
|
+
Substring(s) to search for in the string.
|
|
321
|
+
replace : str or Iterable[str]
|
|
322
|
+
Replacement string(s) for each search substring.
|
|
323
|
+
case_sensitive : bool, optional
|
|
324
|
+
If True, perform case-sensitive replacement. Default is True.
|
|
325
|
+
|
|
326
|
+
Returns
|
|
327
|
+
-------
|
|
328
|
+
Stringable
|
|
329
|
+
New Stringable instance with the specified replacements applied.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
# Convert search and replace to lists for consistent processing
|
|
333
|
+
s = self
|
|
334
|
+
if isinstance(search, str):
|
|
335
|
+
search = [search]
|
|
336
|
+
if isinstance(replace, str):
|
|
337
|
+
replace = [replace] * len(search)
|
|
338
|
+
|
|
339
|
+
# Iterate through each search-replace pair and apply replacement
|
|
340
|
+
for src, rep in zip(search, replace):
|
|
341
|
+
|
|
342
|
+
# Case-sensitive replacement using str.replace
|
|
343
|
+
if case_sensitive:
|
|
344
|
+
s = str(s).replace(src, rep)
|
|
345
|
+
# Case-insensitive replacement using re.sub with IGNORECASE
|
|
346
|
+
else:
|
|
347
|
+
s = re.sub(re.escape(src), rep, str(s), flags=re.IGNORECASE)
|
|
348
|
+
|
|
349
|
+
# Return a new Stringable instance with replacements
|
|
350
|
+
return Stringable(s)
|
|
351
|
+
|
|
352
|
+
def stripTags(self, allowed_tags: Optional[str] = None) -> "Stringable":
|
|
353
|
+
"""
|
|
354
|
+
Remove HTML and PHP tags from the string.
|
|
355
|
+
|
|
356
|
+
Parameters
|
|
357
|
+
----------
|
|
358
|
+
allowed_tags : str, optional
|
|
359
|
+
Tags that should not be stripped. Default is None.
|
|
360
|
+
|
|
361
|
+
Returns
|
|
362
|
+
-------
|
|
363
|
+
Stringable
|
|
364
|
+
New Stringable with tags removed.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
# If allowed_tags is specified, use a simple unescape (not full PHP compatibility)
|
|
368
|
+
if allowed_tags:
|
|
369
|
+
return Stringable(html.unescape(str(self)))
|
|
370
|
+
# Otherwise, remove all tags using regex
|
|
371
|
+
else:
|
|
372
|
+
return Stringable(re.sub(r'<[^>]*>', '', str(self)))
|
|
373
|
+
|
|
374
|
+
def toBase64(self) -> "Stringable":
|
|
375
|
+
"""
|
|
376
|
+
Encode the string to Base64.
|
|
377
|
+
|
|
378
|
+
Returns
|
|
379
|
+
-------
|
|
380
|
+
Stringable
|
|
381
|
+
New Stringable with Base64 encoded content.
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
# Encode the string to Base64
|
|
385
|
+
return Stringable(base64.b64encode(str(self).encode()).decode())
|
|
386
|
+
|
|
387
|
+
def fromBase64(self, strict: bool = False) -> "Stringable":
|
|
388
|
+
"""
|
|
389
|
+
Decode the string from Base64.
|
|
390
|
+
|
|
391
|
+
Parameters
|
|
392
|
+
----------
|
|
393
|
+
strict : bool, optional
|
|
394
|
+
If True, raise exception on decode errors. Default is False.
|
|
395
|
+
|
|
396
|
+
Returns
|
|
397
|
+
-------
|
|
398
|
+
Stringable
|
|
399
|
+
New Stringable with Base64 decoded content, or empty string if decoding fails and strict is False.
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
# Try to decode the string from Base64
|
|
403
|
+
try:
|
|
404
|
+
return Stringable(base64.b64decode(str(self).encode()).decode())
|
|
405
|
+
except Exception:
|
|
406
|
+
if strict:
|
|
407
|
+
raise
|
|
408
|
+
# Return empty string if decoding fails and strict is False
|
|
409
|
+
return Stringable("")
|
|
410
|
+
|
|
411
|
+
def md5(self) -> str:
|
|
412
|
+
"""
|
|
413
|
+
Generate MD5 hash of the string.
|
|
414
|
+
|
|
415
|
+
Returns
|
|
416
|
+
-------
|
|
417
|
+
str
|
|
418
|
+
MD5 hash of the string.
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
# Generate MD5 hash using hashlib
|
|
422
|
+
return hashlib.md5(str(self).encode()).hexdigest()
|
|
423
|
+
|
|
424
|
+
def sha1(self) -> str:
|
|
425
|
+
"""
|
|
426
|
+
Generate SHA1 hash of the string.
|
|
427
|
+
|
|
428
|
+
Returns
|
|
429
|
+
-------
|
|
430
|
+
str
|
|
431
|
+
SHA1 hash of the string.
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
# Generate SHA1 hash using hashlib
|
|
435
|
+
return hashlib.sha1(str(self).encode()).hexdigest()
|
|
436
|
+
|
|
437
|
+
def sha256(self) -> str:
|
|
438
|
+
"""
|
|
439
|
+
Generate SHA256 hash of the string.
|
|
440
|
+
|
|
441
|
+
Returns
|
|
442
|
+
-------
|
|
443
|
+
str
|
|
444
|
+
SHA256 hash of the string.
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
# Generate SHA256 hash using hashlib
|
|
448
|
+
return hashlib.sha256(str(self).encode()).hexdigest()
|
|
449
|
+
|
|
450
|
+
def length(self) -> int:
|
|
451
|
+
"""
|
|
452
|
+
Get the length of the string.
|
|
453
|
+
|
|
454
|
+
Returns
|
|
455
|
+
-------
|
|
456
|
+
int
|
|
457
|
+
Number of characters in the string.
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
# Return the length of the string
|
|
461
|
+
return len(self)
|
|
462
|
+
|
|
463
|
+
def value(self) -> str:
|
|
464
|
+
"""
|
|
465
|
+
Get the string value.
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
str
|
|
470
|
+
String representation of the current instance.
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
# Return the string representation
|
|
474
|
+
return str(self)
|
|
475
|
+
|
|
476
|
+
def toString(self) -> str:
|
|
477
|
+
"""
|
|
478
|
+
Convert the Stringable to a string.
|
|
479
|
+
|
|
480
|
+
Returns
|
|
481
|
+
-------
|
|
482
|
+
str
|
|
483
|
+
String representation of the current instance.
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
# Return the string representation
|
|
487
|
+
return str(self)
|
|
488
|
+
|
|
489
|
+
def toInteger(self, base: int = 10) -> int:
|
|
490
|
+
"""
|
|
491
|
+
Convert the string to an integer.
|
|
492
|
+
|
|
493
|
+
Parameters
|
|
494
|
+
----------
|
|
495
|
+
base : int, optional
|
|
496
|
+
Base for conversion. Default is 10.
|
|
497
|
+
|
|
498
|
+
Returns
|
|
499
|
+
-------
|
|
500
|
+
int
|
|
501
|
+
Integer representation of the string.
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
# Convert the string to integer using the specified base
|
|
505
|
+
return int(self, base)
|
|
506
|
+
|
|
507
|
+
def toFloat(self) -> float:
|
|
508
|
+
"""
|
|
509
|
+
Convert the string to a float.
|
|
510
|
+
|
|
511
|
+
Returns
|
|
512
|
+
-------
|
|
513
|
+
float
|
|
514
|
+
Float representation of the string.
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
# Convert the string to float
|
|
518
|
+
return float(self)
|
|
519
|
+
|
|
520
|
+
def toBoolean(self) -> bool:
|
|
521
|
+
"""
|
|
522
|
+
Convert the string to a boolean.
|
|
523
|
+
|
|
524
|
+
The string is considered True if it matches common truthy values like "1", "true", "on", or "yes" (case-insensitive).
|
|
525
|
+
|
|
526
|
+
Returns
|
|
527
|
+
-------
|
|
528
|
+
bool
|
|
529
|
+
Boolean representation of the string.
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
# Check for common truthy values
|
|
533
|
+
return str(self).strip().lower() in ("1", "true", "on", "yes")
|
|
534
|
+
|
|
535
|
+
def __getitem__(self, key):
|
|
536
|
+
"""
|
|
537
|
+
Get item by index or slice.
|
|
538
|
+
|
|
539
|
+
Parameters
|
|
540
|
+
----------
|
|
541
|
+
key : int or slice
|
|
542
|
+
Index or slice to retrieve.
|
|
543
|
+
|
|
544
|
+
Returns
|
|
545
|
+
-------
|
|
546
|
+
Stringable
|
|
547
|
+
New Stringable instance for the selected item(s).
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
# Return a Stringable for the selected item(s)
|
|
551
|
+
return Stringable(super().__getitem__(key))
|
|
552
|
+
|
|
553
|
+
def __str__(self):
|
|
554
|
+
"""
|
|
555
|
+
Get the string representation.
|
|
556
|
+
|
|
557
|
+
Returns
|
|
558
|
+
-------
|
|
559
|
+
str
|
|
560
|
+
String representation of the object.
|
|
561
|
+
"""
|
|
562
|
+
|
|
563
|
+
# Return the string representation
|
|
564
|
+
return super().__str__()
|
|
565
|
+
|
|
566
|
+
def isAlnum(self) -> bool:
|
|
567
|
+
"""
|
|
568
|
+
Check if all characters in the string are alphanumeric.
|
|
569
|
+
|
|
570
|
+
Returns
|
|
571
|
+
-------
|
|
572
|
+
bool
|
|
573
|
+
True if all characters are alphanumeric, False otherwise.
|
|
574
|
+
"""
|
|
575
|
+
|
|
576
|
+
# Check if all characters are alphanumeric
|
|
577
|
+
return str(self).isalnum()
|
|
578
|
+
|
|
579
|
+
def isAlpha(self) -> bool:
|
|
580
|
+
"""
|
|
581
|
+
Check if all characters in the string are alphabetic.
|
|
582
|
+
|
|
583
|
+
Returns
|
|
584
|
+
-------
|
|
585
|
+
bool
|
|
586
|
+
True if all characters are alphabetic, False otherwise.
|
|
587
|
+
"""
|
|
588
|
+
|
|
589
|
+
# Check if all characters are alphabetic
|
|
590
|
+
return str(self).isalpha()
|
|
591
|
+
|
|
592
|
+
def isDecimal(self) -> bool:
|
|
593
|
+
"""
|
|
594
|
+
Check if all characters in the string are decimal characters.
|
|
595
|
+
|
|
596
|
+
Returns
|
|
597
|
+
-------
|
|
598
|
+
bool
|
|
599
|
+
True if all characters are decimal, False otherwise.
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
# Check if all characters are decimal
|
|
603
|
+
return str(self).isdecimal()
|
|
604
|
+
|
|
605
|
+
def isDigit(self) -> bool:
|
|
606
|
+
"""
|
|
607
|
+
Check if all characters in the string are digits.
|
|
608
|
+
|
|
609
|
+
Returns
|
|
610
|
+
-------
|
|
611
|
+
bool
|
|
612
|
+
True if all characters are digits, False otherwise.
|
|
613
|
+
"""
|
|
614
|
+
|
|
615
|
+
# Check if all characters are digits
|
|
616
|
+
return str(self).isdigit()
|
|
617
|
+
|
|
618
|
+
def isIdentifier(self) -> bool:
|
|
619
|
+
"""
|
|
620
|
+
Check if the string is a valid identifier according to Python language definition.
|
|
621
|
+
|
|
622
|
+
Returns
|
|
623
|
+
-------
|
|
624
|
+
bool
|
|
625
|
+
True if string is a valid identifier, False otherwise.
|
|
626
|
+
"""
|
|
627
|
+
|
|
628
|
+
# Check if the string is a valid identifier
|
|
629
|
+
return str(self).isidentifier()
|
|
630
|
+
|
|
631
|
+
def isLower(self) -> bool:
|
|
632
|
+
"""
|
|
633
|
+
Check if all cased characters in the string are lowercase.
|
|
634
|
+
|
|
635
|
+
Returns
|
|
636
|
+
-------
|
|
637
|
+
bool
|
|
638
|
+
True if all cased characters are lowercase, False otherwise.
|
|
639
|
+
"""
|
|
640
|
+
|
|
641
|
+
# Check if all cased characters are lowercase
|
|
642
|
+
return str(self).islower()
|
|
643
|
+
|
|
644
|
+
def isNumeric(self) -> bool:
|
|
645
|
+
"""
|
|
646
|
+
Check if all characters in the string are numeric characters.
|
|
647
|
+
|
|
648
|
+
Returns
|
|
649
|
+
-------
|
|
650
|
+
bool
|
|
651
|
+
True if all characters are numeric, False otherwise.
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
# Check if all characters are numeric
|
|
655
|
+
return str(self).isnumeric()
|
|
656
|
+
|
|
657
|
+
def isPrintable(self) -> bool:
|
|
658
|
+
"""
|
|
659
|
+
Check if all characters in the string are printable.
|
|
660
|
+
|
|
661
|
+
Returns
|
|
662
|
+
-------
|
|
663
|
+
bool
|
|
664
|
+
True if all characters are printable, False otherwise.
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
# Check if all characters are printable
|
|
668
|
+
return str(self).isprintable()
|
|
669
|
+
|
|
670
|
+
def isSpace(self) -> bool:
|
|
671
|
+
"""
|
|
672
|
+
Check if there are only whitespace characters in the string.
|
|
673
|
+
|
|
674
|
+
Returns
|
|
675
|
+
-------
|
|
676
|
+
bool
|
|
677
|
+
True if string contains only whitespace, False otherwise.
|
|
678
|
+
"""
|
|
679
|
+
|
|
680
|
+
# Check if the string contains only whitespace
|
|
681
|
+
return str(self).isspace()
|
|
682
|
+
|
|
683
|
+
def isTitle(self) -> bool:
|
|
684
|
+
"""
|
|
685
|
+
Check if the string is a titlecased string.
|
|
686
|
+
|
|
687
|
+
Returns
|
|
688
|
+
-------
|
|
689
|
+
bool
|
|
690
|
+
True if string is titlecased, False otherwise.
|
|
691
|
+
"""
|
|
692
|
+
|
|
693
|
+
# Check if the string is titlecased
|
|
694
|
+
return str(self).istitle()
|
|
695
|
+
|
|
696
|
+
def isUpper(self) -> bool:
|
|
697
|
+
"""
|
|
698
|
+
Check if all cased characters in the string are uppercase.
|
|
699
|
+
|
|
700
|
+
Returns
|
|
701
|
+
-------
|
|
702
|
+
bool
|
|
703
|
+
True if all cased characters are uppercase, False otherwise.
|
|
704
|
+
"""
|
|
705
|
+
|
|
706
|
+
# Check if all cased characters are uppercase
|
|
707
|
+
return str(self).isupper()
|
|
708
|
+
|
|
709
|
+
def lStrip(self, chars: Optional[str] = None) -> "Stringable":
|
|
710
|
+
"""
|
|
711
|
+
Return a copy of the string with leading characters removed.
|
|
712
|
+
|
|
713
|
+
Removes leading characters from the left side of the string. If no characters
|
|
714
|
+
are specified, whitespace characters are removed by default.
|
|
715
|
+
|
|
716
|
+
Parameters
|
|
717
|
+
----------
|
|
718
|
+
chars : str, optional
|
|
719
|
+
Characters to remove from the beginning, by default None (whitespace).
|
|
720
|
+
|
|
721
|
+
Returns
|
|
722
|
+
-------
|
|
723
|
+
Stringable
|
|
724
|
+
A new Stringable instance with leading characters removed.
|
|
725
|
+
"""
|
|
726
|
+
|
|
727
|
+
# Use Python's built-in lstrip to remove leading characters
|
|
728
|
+
return Stringable(str(self).lstrip(chars))
|
|
729
|
+
|
|
730
|
+
def rStrip(self, chars: Optional[str] = None) -> "Stringable":
|
|
731
|
+
"""
|
|
732
|
+
Return a copy of the string with trailing characters removed.
|
|
733
|
+
|
|
734
|
+
Removes trailing characters from the right side of the string. If no characters
|
|
735
|
+
are specified, whitespace characters are removed by default.
|
|
736
|
+
|
|
737
|
+
Parameters
|
|
738
|
+
----------
|
|
739
|
+
chars : str, optional
|
|
740
|
+
Characters to remove from the end, by default None (whitespace).
|
|
741
|
+
|
|
742
|
+
Returns
|
|
743
|
+
-------
|
|
744
|
+
Stringable
|
|
745
|
+
A new Stringable instance with trailing characters removed.
|
|
746
|
+
"""
|
|
747
|
+
|
|
748
|
+
# Use Python's built-in rstrip to remove trailing characters
|
|
749
|
+
return Stringable(str(self).rstrip(chars))
|
|
750
|
+
|
|
751
|
+
def swapCase(self) -> "Stringable":
|
|
752
|
+
"""
|
|
753
|
+
Return a copy of the string with uppercase characters converted to lowercase and vice versa.
|
|
754
|
+
|
|
755
|
+
Converts each uppercase character to lowercase and each lowercase character
|
|
756
|
+
to uppercase, leaving other characters unchanged.
|
|
757
|
+
|
|
758
|
+
Returns
|
|
759
|
+
-------
|
|
760
|
+
Stringable
|
|
761
|
+
A new Stringable instance with all character cases swapped.
|
|
762
|
+
"""
|
|
763
|
+
|
|
764
|
+
# Use Python's built-in swapcase to invert the case of all characters
|
|
765
|
+
return Stringable(str(self).swapcase())
|
|
766
|
+
|
|
767
|
+
def zFill(self, width: int) -> "Stringable":
|
|
768
|
+
"""
|
|
769
|
+
Pad a numeric string with zeros on the left.
|
|
770
|
+
|
|
771
|
+
Fills the string with leading zeros to reach the specified width. The sign
|
|
772
|
+
of the number (if any) is handled properly by being placed before the zeros.
|
|
773
|
+
|
|
774
|
+
Parameters
|
|
775
|
+
----------
|
|
776
|
+
width : int
|
|
777
|
+
Total width of the resulting string.
|
|
778
|
+
|
|
779
|
+
Returns
|
|
780
|
+
-------
|
|
781
|
+
Stringable
|
|
782
|
+
A new Stringable instance padded with leading zeros to the specified width.
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
# Use Python's built-in zfill to pad with zeros while preserving sign
|
|
786
|
+
return Stringable(str(self).zfill(width))
|
|
787
|
+
|
|
788
|
+
def ascii(self, language: str = 'en') -> "Stringable":
|
|
789
|
+
"""
|
|
790
|
+
Transliterate a UTF-8 value to ASCII.
|
|
791
|
+
|
|
792
|
+
Parameters
|
|
793
|
+
----------
|
|
794
|
+
language : str, optional
|
|
795
|
+
The language for transliteration, by default 'en'
|
|
796
|
+
|
|
797
|
+
Returns
|
|
798
|
+
-------
|
|
799
|
+
Stringable
|
|
800
|
+
A new Stringable with ASCII characters
|
|
801
|
+
"""
|
|
802
|
+
# Use unicodedata to normalize and transliterate
|
|
803
|
+
normalized = unicodedata.normalize('NFKD', self)
|
|
804
|
+
ascii_str = ''.join(c for c in normalized if ord(c) < 128)
|
|
805
|
+
return Stringable(ascii_str)
|
|
806
|
+
|
|
807
|
+
def camel(self) -> "Stringable":
|
|
808
|
+
"""
|
|
809
|
+
Convert a value to camel case.
|
|
810
|
+
|
|
811
|
+
Returns
|
|
812
|
+
-------
|
|
813
|
+
Stringable
|
|
814
|
+
A new Stringable in camelCase
|
|
815
|
+
"""
|
|
816
|
+
# Split by common separators and normalize
|
|
817
|
+
words = re.sub(r'[_\-\s]+', ' ', str(self)).split()
|
|
818
|
+
if not words:
|
|
819
|
+
return Stringable("")
|
|
820
|
+
|
|
821
|
+
# First word lowercase, rest title case
|
|
822
|
+
camel_str = words[0].lower() + ''.join(word.capitalize() for word in words[1:])
|
|
823
|
+
return Stringable(camel_str)
|
|
824
|
+
|
|
825
|
+
def kebab(self) -> "Stringable":
|
|
826
|
+
"""
|
|
827
|
+
Convert a string to kebab case.
|
|
828
|
+
|
|
829
|
+
Returns
|
|
830
|
+
-------
|
|
831
|
+
Stringable
|
|
832
|
+
A new Stringable in kebab-case
|
|
833
|
+
"""
|
|
834
|
+
# Handle camelCase and PascalCase
|
|
835
|
+
s = re.sub(r'([a-z0-9])([A-Z])', r'\1-\2', str(self))
|
|
836
|
+
# Replace spaces, underscores, and multiple dashes with single dash
|
|
837
|
+
s = re.sub(r'[_\s]+', '-', s)
|
|
838
|
+
s = re.sub(r'-+', '-', s)
|
|
839
|
+
return Stringable(s.lower().strip('-'))
|
|
840
|
+
|
|
841
|
+
def snake(self, delimiter: str = '_') -> "Stringable":
|
|
842
|
+
"""
|
|
843
|
+
Convert a string to snake case.
|
|
844
|
+
|
|
845
|
+
Parameters
|
|
846
|
+
----------
|
|
847
|
+
delimiter : str, optional
|
|
848
|
+
The delimiter to use, by default '_'
|
|
849
|
+
|
|
850
|
+
Returns
|
|
851
|
+
-------
|
|
852
|
+
Stringable
|
|
853
|
+
A new Stringable in snake_case
|
|
854
|
+
"""
|
|
855
|
+
# Handle camelCase and PascalCase
|
|
856
|
+
s = re.sub(r'([a-z0-9])([A-Z])', rf'\1{delimiter}\2', str(self))
|
|
857
|
+
# Replace spaces and dashes with delimiter
|
|
858
|
+
s = re.sub(r'[\s\-]+', delimiter, s)
|
|
859
|
+
# Replace multiple delimiters with single
|
|
860
|
+
s = re.sub(rf'{re.escape(delimiter)}+', delimiter, s)
|
|
861
|
+
return Stringable(s.lower().strip(delimiter))
|
|
862
|
+
|
|
863
|
+
def studly(self) -> "Stringable":
|
|
864
|
+
"""
|
|
865
|
+
Convert a value to studly caps case (PascalCase).
|
|
866
|
+
|
|
867
|
+
Returns
|
|
868
|
+
-------
|
|
869
|
+
Stringable
|
|
870
|
+
A new Stringable in StudlyCase/PascalCase
|
|
871
|
+
"""
|
|
872
|
+
words = re.sub(r'[_\-\s]+', ' ', str(self)).split()
|
|
873
|
+
studly_str = ''.join(word.capitalize() for word in words)
|
|
874
|
+
return Stringable(studly_str)
|
|
875
|
+
|
|
876
|
+
def pascal(self) -> "Stringable":
|
|
877
|
+
"""
|
|
878
|
+
Convert the string to Pascal case.
|
|
879
|
+
|
|
880
|
+
Returns
|
|
881
|
+
-------
|
|
882
|
+
Stringable
|
|
883
|
+
A new Stringable in PascalCase
|
|
884
|
+
"""
|
|
885
|
+
return self.studly()
|
|
886
|
+
|
|
887
|
+
def slug(self, separator: str = '-', language: str = 'en', dictionary: Optional[Dict[str, str]] = None) -> "Stringable":
|
|
888
|
+
"""
|
|
889
|
+
Generate a URL friendly "slug" from a given string.
|
|
890
|
+
|
|
891
|
+
Parameters
|
|
892
|
+
----------
|
|
893
|
+
separator : str, optional
|
|
894
|
+
The separator to use, by default '-'
|
|
895
|
+
language : str, optional
|
|
896
|
+
The language for transliteration, by default 'en'
|
|
897
|
+
dictionary : dict, optional
|
|
898
|
+
Dictionary for character replacements, by default {'@': 'at'}
|
|
899
|
+
|
|
900
|
+
Returns
|
|
901
|
+
-------
|
|
902
|
+
Stringable
|
|
903
|
+
A new Stringable as a URL-friendly slug
|
|
904
|
+
"""
|
|
905
|
+
if dictionary is None:
|
|
906
|
+
dictionary = {'@': 'at'}
|
|
907
|
+
|
|
908
|
+
s = str(self)
|
|
909
|
+
|
|
910
|
+
# Apply dictionary replacements
|
|
911
|
+
for key, value in dictionary.items():
|
|
912
|
+
s = s.replace(key, value)
|
|
913
|
+
|
|
914
|
+
# Convert to ASCII
|
|
915
|
+
s = self.__class__(s).ascii().value()
|
|
916
|
+
|
|
917
|
+
# Remove all non-alphanumeric characters except spaces and separators
|
|
918
|
+
s = re.sub(r'[^\w\s-]', '', s)
|
|
919
|
+
|
|
920
|
+
# Replace spaces and underscores with separator
|
|
921
|
+
s = re.sub(r'[\s_]+', separator, s)
|
|
922
|
+
|
|
923
|
+
# Replace multiple separators with single
|
|
924
|
+
s = re.sub(rf'{re.escape(separator)}+', separator, s)
|
|
925
|
+
|
|
926
|
+
return Stringable(s.lower().strip(separator))
|
|
927
|
+
|
|
928
|
+
def title(self) -> "Stringable":
|
|
929
|
+
"""
|
|
930
|
+
Convert the given string to proper case.
|
|
931
|
+
|
|
932
|
+
Returns
|
|
933
|
+
-------
|
|
934
|
+
Stringable
|
|
935
|
+
A new Stringable in Title Case
|
|
936
|
+
"""
|
|
937
|
+
return Stringable(str(self).title())
|
|
938
|
+
|
|
939
|
+
def headline(self) -> "Stringable":
|
|
940
|
+
"""
|
|
941
|
+
Convert the given string to proper case for each word.
|
|
942
|
+
|
|
943
|
+
Returns
|
|
944
|
+
-------
|
|
945
|
+
Stringable
|
|
946
|
+
A new Stringable as a headline
|
|
947
|
+
"""
|
|
948
|
+
# Split by common word boundaries
|
|
949
|
+
words = re.findall(r'\b\w+\b', str(self))
|
|
950
|
+
headline_str = ' '.join(word.capitalize() for word in words)
|
|
951
|
+
return Stringable(headline_str)
|
|
952
|
+
|
|
953
|
+
def apa(self) -> "Stringable":
|
|
954
|
+
"""
|
|
955
|
+
Convert the given string to APA-style title case.
|
|
956
|
+
|
|
957
|
+
Returns
|
|
958
|
+
-------
|
|
959
|
+
Stringable
|
|
960
|
+
A new Stringable in APA title case
|
|
961
|
+
"""
|
|
962
|
+
# Words that should not be capitalized in APA style (except at beginning)
|
|
963
|
+
lowercase_words = {
|
|
964
|
+
'a', 'an', 'and', 'as', 'at', 'but', 'by', 'for', 'if', 'in',
|
|
965
|
+
'nor', 'of', 'on', 'or', 'so', 'the', 'to', 'up', 'yet'
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
words = str(self).split()
|
|
969
|
+
apa_words = []
|
|
970
|
+
|
|
971
|
+
for i, word in enumerate(words):
|
|
972
|
+
# Always capitalize first and last word
|
|
973
|
+
if i == 0 or i == len(words) - 1:
|
|
974
|
+
apa_words.append(word.capitalize())
|
|
975
|
+
# Capitalize words with 4+ letters or not in lowercase set
|
|
976
|
+
elif len(word) >= 4 or word.lower() not in lowercase_words:
|
|
977
|
+
apa_words.append(word.capitalize())
|
|
978
|
+
else:
|
|
979
|
+
apa_words.append(word.lower())
|
|
980
|
+
|
|
981
|
+
return Stringable(' '.join(apa_words))
|
|
982
|
+
|
|
983
|
+
def ucfirst(self) -> "Stringable":
|
|
984
|
+
"""
|
|
985
|
+
Make a string's first character uppercase.
|
|
986
|
+
|
|
987
|
+
Returns
|
|
988
|
+
-------
|
|
989
|
+
Stringable
|
|
990
|
+
A new Stringable with first character uppercase
|
|
991
|
+
"""
|
|
992
|
+
if not self:
|
|
993
|
+
return Stringable(self)
|
|
994
|
+
return Stringable(self[0].upper() + self[1:])
|
|
995
|
+
|
|
996
|
+
def lcfirst(self) -> "Stringable":
|
|
997
|
+
"""
|
|
998
|
+
Make a string's first character lowercase.
|
|
999
|
+
|
|
1000
|
+
Returns
|
|
1001
|
+
-------
|
|
1002
|
+
Stringable
|
|
1003
|
+
A new Stringable with first character lowercase
|
|
1004
|
+
"""
|
|
1005
|
+
if not self:
|
|
1006
|
+
return Stringable(self)
|
|
1007
|
+
return Stringable(self[0].lower() + self[1:])
|
|
1008
|
+
|
|
1009
|
+
def isAscii(self) -> bool:
|
|
1010
|
+
"""
|
|
1011
|
+
Determine if a given string is 7 bit ASCII.
|
|
1012
|
+
|
|
1013
|
+
Returns
|
|
1014
|
+
-------
|
|
1015
|
+
bool
|
|
1016
|
+
True if string is ASCII, False otherwise
|
|
1017
|
+
"""
|
|
1018
|
+
try:
|
|
1019
|
+
self.encode('ascii')
|
|
1020
|
+
return True
|
|
1021
|
+
except UnicodeEncodeError:
|
|
1022
|
+
return False
|
|
1023
|
+
|
|
1024
|
+
def isJson(self) -> bool:
|
|
1025
|
+
"""
|
|
1026
|
+
Determine if a given string is valid JSON.
|
|
1027
|
+
|
|
1028
|
+
Returns
|
|
1029
|
+
-------
|
|
1030
|
+
bool
|
|
1031
|
+
True if string is valid JSON, False otherwise
|
|
1032
|
+
"""
|
|
1033
|
+
try:
|
|
1034
|
+
json.loads(str(self))
|
|
1035
|
+
return True
|
|
1036
|
+
except (json.JSONDecodeError, TypeError):
|
|
1037
|
+
return False
|
|
1038
|
+
|
|
1039
|
+
def isUrl(self, protocols: Optional[List[str]] = None) -> bool:
|
|
1040
|
+
"""
|
|
1041
|
+
Determine if a given value is a valid URL.
|
|
1042
|
+
|
|
1043
|
+
Parameters
|
|
1044
|
+
----------
|
|
1045
|
+
protocols : list, optional
|
|
1046
|
+
List of valid protocols, by default ['http', 'https']
|
|
1047
|
+
|
|
1048
|
+
Returns
|
|
1049
|
+
-------
|
|
1050
|
+
bool
|
|
1051
|
+
True if string is a valid URL, False otherwise
|
|
1052
|
+
"""
|
|
1053
|
+
if protocols is None:
|
|
1054
|
+
protocols = ['http', 'https']
|
|
1055
|
+
|
|
1056
|
+
try:
|
|
1057
|
+
result = urllib.parse.urlparse(str(self))
|
|
1058
|
+
return (
|
|
1059
|
+
all([result.scheme, result.netloc]) and
|
|
1060
|
+
result.scheme in protocols
|
|
1061
|
+
)
|
|
1062
|
+
except Exception:
|
|
1063
|
+
return False
|
|
1064
|
+
|
|
1065
|
+
def isUuid(self, version: Optional[Union[int, str]] = None) -> bool:
|
|
1066
|
+
"""
|
|
1067
|
+
Determine if a given string is a valid UUID.
|
|
1068
|
+
|
|
1069
|
+
Parameters
|
|
1070
|
+
----------
|
|
1071
|
+
version : int or str, optional
|
|
1072
|
+
UUID version to validate (1-8), by default None (any version)
|
|
1073
|
+
|
|
1074
|
+
Returns
|
|
1075
|
+
-------
|
|
1076
|
+
bool
|
|
1077
|
+
True if string is a valid UUID, False otherwise
|
|
1078
|
+
"""
|
|
1079
|
+
try:
|
|
1080
|
+
uuid_obj = uuid.UUID(str(self))
|
|
1081
|
+
if version is not None:
|
|
1082
|
+
if version == 'max':
|
|
1083
|
+
return uuid_obj.version <= 8
|
|
1084
|
+
else:
|
|
1085
|
+
return uuid_obj.version == int(version)
|
|
1086
|
+
return True
|
|
1087
|
+
except (ValueError, TypeError):
|
|
1088
|
+
return False
|
|
1089
|
+
|
|
1090
|
+
def isUlid(self) -> bool:
|
|
1091
|
+
"""
|
|
1092
|
+
Determine if a given string is a valid ULID.
|
|
1093
|
+
|
|
1094
|
+
Returns
|
|
1095
|
+
-------
|
|
1096
|
+
bool
|
|
1097
|
+
True if string is a valid ULID, False otherwise
|
|
1098
|
+
"""
|
|
1099
|
+
# ULID is 26 characters, base32 encoded
|
|
1100
|
+
ulid_pattern = r'^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$'
|
|
1101
|
+
return bool(re.match(ulid_pattern, str(self).upper()))
|
|
1102
|
+
|
|
1103
|
+
def chopStart(self, needle: Union[str, List[str]]) -> "Stringable":
|
|
1104
|
+
"""
|
|
1105
|
+
Remove the given string if it exists at the start of the current string.
|
|
1106
|
+
|
|
1107
|
+
Parameters
|
|
1108
|
+
----------
|
|
1109
|
+
needle : str or list
|
|
1110
|
+
The string(s) to remove from the start
|
|
1111
|
+
|
|
1112
|
+
Returns
|
|
1113
|
+
-------
|
|
1114
|
+
Stringable
|
|
1115
|
+
A new Stringable with the needle removed from start
|
|
1116
|
+
"""
|
|
1117
|
+
s = str(self)
|
|
1118
|
+
if isinstance(needle, str):
|
|
1119
|
+
needle = [needle]
|
|
1120
|
+
|
|
1121
|
+
for n in needle:
|
|
1122
|
+
if s.startswith(n):
|
|
1123
|
+
s = s[len(n):]
|
|
1124
|
+
break
|
|
1125
|
+
|
|
1126
|
+
return Stringable(s)
|
|
1127
|
+
|
|
1128
|
+
def chopEnd(self, needle: Union[str, List[str]]) -> "Stringable":
|
|
1129
|
+
"""
|
|
1130
|
+
Remove the given string if it exists at the end of the current string.
|
|
1131
|
+
|
|
1132
|
+
Parameters
|
|
1133
|
+
----------
|
|
1134
|
+
needle : str or list
|
|
1135
|
+
The string(s) to remove from the end
|
|
1136
|
+
|
|
1137
|
+
Returns
|
|
1138
|
+
-------
|
|
1139
|
+
Stringable
|
|
1140
|
+
A new Stringable with the needle removed from end
|
|
1141
|
+
"""
|
|
1142
|
+
s = str(self)
|
|
1143
|
+
if isinstance(needle, str):
|
|
1144
|
+
needle = [needle]
|
|
1145
|
+
|
|
1146
|
+
for n in needle:
|
|
1147
|
+
if s.endswith(n):
|
|
1148
|
+
s = s[:-len(n)]
|
|
1149
|
+
break
|
|
1150
|
+
|
|
1151
|
+
return Stringable(s)
|
|
1152
|
+
|
|
1153
|
+
def deduplicate(self, character: str = ' ') -> "Stringable":
|
|
1154
|
+
"""
|
|
1155
|
+
Replace consecutive instances of a given character with a single character.
|
|
1156
|
+
|
|
1157
|
+
Parameters
|
|
1158
|
+
----------
|
|
1159
|
+
character : str, optional
|
|
1160
|
+
The character to deduplicate, by default ' '
|
|
1161
|
+
|
|
1162
|
+
Returns
|
|
1163
|
+
-------
|
|
1164
|
+
Stringable
|
|
1165
|
+
A new Stringable with deduplicated characters
|
|
1166
|
+
"""
|
|
1167
|
+
pattern = re.escape(character) + '+'
|
|
1168
|
+
return Stringable(re.sub(pattern, character, str(self)))
|
|
1169
|
+
|
|
1170
|
+
def mask(self, character: str, index: int, length: Optional[int] = None) -> "Stringable":
|
|
1171
|
+
"""
|
|
1172
|
+
Masks a portion of a string with a repeated character.
|
|
1173
|
+
|
|
1174
|
+
Parameters
|
|
1175
|
+
----------
|
|
1176
|
+
character : str
|
|
1177
|
+
The character to use for masking
|
|
1178
|
+
index : int
|
|
1179
|
+
Starting index for masking
|
|
1180
|
+
length : int, optional
|
|
1181
|
+
Length to mask, by default None (to end of string)
|
|
1182
|
+
|
|
1183
|
+
Returns
|
|
1184
|
+
-------
|
|
1185
|
+
Stringable
|
|
1186
|
+
A new Stringable with masked portion
|
|
1187
|
+
"""
|
|
1188
|
+
s = str(self)
|
|
1189
|
+
|
|
1190
|
+
if index < 0:
|
|
1191
|
+
index = max(0, len(s) + index)
|
|
1192
|
+
|
|
1193
|
+
if length is None:
|
|
1194
|
+
length = len(s) - index
|
|
1195
|
+
elif length < 0:
|
|
1196
|
+
length = max(0, len(s) + length - index)
|
|
1197
|
+
|
|
1198
|
+
end_index = min(len(s), index + length)
|
|
1199
|
+
mask_str = character * (end_index - index)
|
|
1200
|
+
|
|
1201
|
+
return Stringable(s[:index] + mask_str + s[end_index:])
|
|
1202
|
+
|
|
1203
|
+
def limit(self, limit: int = 100, end: str = '...', preserve_words: bool = False) -> "Stringable":
|
|
1204
|
+
"""
|
|
1205
|
+
Limit the number of characters in a string.
|
|
1206
|
+
|
|
1207
|
+
Parameters
|
|
1208
|
+
----------
|
|
1209
|
+
limit : int, optional
|
|
1210
|
+
Maximum number of characters, by default 100
|
|
1211
|
+
end : str, optional
|
|
1212
|
+
String to append if truncated, by default '...'
|
|
1213
|
+
preserve_words : bool, optional
|
|
1214
|
+
Whether to preserve word boundaries, by default False
|
|
1215
|
+
|
|
1216
|
+
Returns
|
|
1217
|
+
-------
|
|
1218
|
+
Stringable
|
|
1219
|
+
A new Stringable with limited length
|
|
1220
|
+
"""
|
|
1221
|
+
s = str(self)
|
|
1222
|
+
|
|
1223
|
+
if len(s) <= limit:
|
|
1224
|
+
return Stringable(s)
|
|
1225
|
+
|
|
1226
|
+
if preserve_words:
|
|
1227
|
+
# Find the last space before the limit
|
|
1228
|
+
truncated = s[:limit]
|
|
1229
|
+
last_space = truncated.rfind(' ')
|
|
1230
|
+
if last_space > 0:
|
|
1231
|
+
truncated = truncated[:last_space]
|
|
1232
|
+
else:
|
|
1233
|
+
truncated = s[:limit]
|
|
1234
|
+
|
|
1235
|
+
return Stringable(truncated + end)
|
|
1236
|
+
|
|
1237
|
+
def padBoth(self, length: int, pad: str = ' ') -> "Stringable":
|
|
1238
|
+
"""
|
|
1239
|
+
Pad both sides of the string with another.
|
|
1240
|
+
|
|
1241
|
+
Parameters
|
|
1242
|
+
----------
|
|
1243
|
+
length : int
|
|
1244
|
+
Total desired length
|
|
1245
|
+
pad : str, optional
|
|
1246
|
+
Padding character(s), by default ' '
|
|
1247
|
+
|
|
1248
|
+
Returns
|
|
1249
|
+
-------
|
|
1250
|
+
Stringable
|
|
1251
|
+
A new Stringable with padding on both sides
|
|
1252
|
+
"""
|
|
1253
|
+
s = str(self)
|
|
1254
|
+
if len(s) >= length:
|
|
1255
|
+
return Stringable(s)
|
|
1256
|
+
|
|
1257
|
+
total_padding = length - len(s)
|
|
1258
|
+
left_padding = total_padding // 2
|
|
1259
|
+
right_padding = total_padding - left_padding
|
|
1260
|
+
|
|
1261
|
+
left_pad = (pad * ((left_padding // len(pad)) + 1))[:left_padding]
|
|
1262
|
+
right_pad = (pad * ((right_padding // len(pad)) + 1))[:right_padding]
|
|
1263
|
+
|
|
1264
|
+
return Stringable(left_pad + s + right_pad)
|
|
1265
|
+
|
|
1266
|
+
def padLeft(self, length: int, pad: str = ' ') -> "Stringable":
|
|
1267
|
+
"""
|
|
1268
|
+
Pad the left side of the string with another.
|
|
1269
|
+
|
|
1270
|
+
Parameters
|
|
1271
|
+
----------
|
|
1272
|
+
length : int
|
|
1273
|
+
Total desired length
|
|
1274
|
+
pad : str, optional
|
|
1275
|
+
Padding character(s), by default ' '
|
|
1276
|
+
|
|
1277
|
+
Returns
|
|
1278
|
+
-------
|
|
1279
|
+
Stringable
|
|
1280
|
+
A new Stringable with left padding
|
|
1281
|
+
"""
|
|
1282
|
+
s = str(self)
|
|
1283
|
+
if len(s) >= length:
|
|
1284
|
+
return Stringable(s)
|
|
1285
|
+
|
|
1286
|
+
padding_needed = length - len(s)
|
|
1287
|
+
left_pad = (pad * ((padding_needed // len(pad)) + 1))[:padding_needed]
|
|
1288
|
+
|
|
1289
|
+
return Stringable(left_pad + s)
|
|
1290
|
+
|
|
1291
|
+
def padRight(self, length: int, pad: str = ' ') -> "Stringable":
|
|
1292
|
+
"""
|
|
1293
|
+
Pad the right side of the string with another.
|
|
1294
|
+
|
|
1295
|
+
Parameters
|
|
1296
|
+
----------
|
|
1297
|
+
length : int
|
|
1298
|
+
Total desired length
|
|
1299
|
+
pad : str, optional
|
|
1300
|
+
Padding character(s), by default ' '
|
|
1301
|
+
|
|
1302
|
+
Returns
|
|
1303
|
+
-------
|
|
1304
|
+
Stringable
|
|
1305
|
+
A new Stringable with right padding
|
|
1306
|
+
"""
|
|
1307
|
+
s = str(self)
|
|
1308
|
+
if len(s) >= length:
|
|
1309
|
+
return Stringable(s)
|
|
1310
|
+
|
|
1311
|
+
padding_needed = length - len(s)
|
|
1312
|
+
right_pad = (pad * ((padding_needed // len(pad)) + 1))[:padding_needed]
|
|
1313
|
+
|
|
1314
|
+
return Stringable(s + right_pad)
|
|
1315
|
+
|
|
1316
|
+
def trim(self, characters: Optional[str] = None) -> "Stringable":
|
|
1317
|
+
"""
|
|
1318
|
+
Trim the string of the given characters.
|
|
1319
|
+
|
|
1320
|
+
Parameters
|
|
1321
|
+
----------
|
|
1322
|
+
characters : str, optional
|
|
1323
|
+
Characters to trim, by default None (whitespace)
|
|
1324
|
+
|
|
1325
|
+
Returns
|
|
1326
|
+
-------
|
|
1327
|
+
Stringable
|
|
1328
|
+
A new trimmed Stringable
|
|
1329
|
+
"""
|
|
1330
|
+
return Stringable(str(self).strip(characters))
|
|
1331
|
+
|
|
1332
|
+
def ltrim(self, characters: Optional[str] = None) -> "Stringable":
|
|
1333
|
+
"""
|
|
1334
|
+
Left trim the string of the given characters.
|
|
1335
|
+
|
|
1336
|
+
Parameters
|
|
1337
|
+
----------
|
|
1338
|
+
characters : str, optional
|
|
1339
|
+
Characters to trim, by default None (whitespace)
|
|
1340
|
+
|
|
1341
|
+
Returns
|
|
1342
|
+
-------
|
|
1343
|
+
Stringable
|
|
1344
|
+
A new left-trimmed Stringable
|
|
1345
|
+
"""
|
|
1346
|
+
return Stringable(str(self).lstrip(characters))
|
|
1347
|
+
|
|
1348
|
+
def rtrim(self, characters: Optional[str] = None) -> "Stringable":
|
|
1349
|
+
"""
|
|
1350
|
+
Right trim the string of the given characters.
|
|
1351
|
+
|
|
1352
|
+
Parameters
|
|
1353
|
+
----------
|
|
1354
|
+
characters : str, optional
|
|
1355
|
+
Characters to trim, by default None (whitespace)
|
|
1356
|
+
|
|
1357
|
+
Returns
|
|
1358
|
+
-------
|
|
1359
|
+
Stringable
|
|
1360
|
+
A new right-trimmed Stringable
|
|
1361
|
+
"""
|
|
1362
|
+
return Stringable(str(self).rstrip(characters))
|
|
1363
|
+
|
|
1364
|
+
def charAt(self, index: int) -> Union[str, bool]:
|
|
1365
|
+
"""
|
|
1366
|
+
Get the character at the specified index.
|
|
1367
|
+
|
|
1368
|
+
Parameters
|
|
1369
|
+
----------
|
|
1370
|
+
index : int
|
|
1371
|
+
The index of the character to get
|
|
1372
|
+
|
|
1373
|
+
Returns
|
|
1374
|
+
-------
|
|
1375
|
+
str or False
|
|
1376
|
+
The character at the index, or False if index is out of bounds
|
|
1377
|
+
"""
|
|
1378
|
+
try:
|
|
1379
|
+
return str(self)[index]
|
|
1380
|
+
except IndexError:
|
|
1381
|
+
return False
|
|
1382
|
+
|
|
1383
|
+
def position(self, needle: str, offset: int = 0, encoding: Optional[str] = None) -> Union[int, bool]:
|
|
1384
|
+
"""
|
|
1385
|
+
Find the multi-byte safe position of the first occurrence of the given substring.
|
|
1386
|
+
|
|
1387
|
+
Parameters
|
|
1388
|
+
----------
|
|
1389
|
+
needle : str
|
|
1390
|
+
The substring to search for
|
|
1391
|
+
offset : int, optional
|
|
1392
|
+
Starting offset for search, by default 0
|
|
1393
|
+
encoding : str, optional
|
|
1394
|
+
String encoding (for compatibility), by default None
|
|
1395
|
+
|
|
1396
|
+
Returns
|
|
1397
|
+
-------
|
|
1398
|
+
int or False
|
|
1399
|
+
Position of the substring, or False if not found
|
|
1400
|
+
"""
|
|
1401
|
+
try:
|
|
1402
|
+
pos = str(self).find(needle, offset)
|
|
1403
|
+
return pos if pos != -1 else False
|
|
1404
|
+
except Exception:
|
|
1405
|
+
return False
|
|
1406
|
+
|
|
1407
|
+
def match(self, pattern: str) -> "Stringable":
|
|
1408
|
+
"""
|
|
1409
|
+
Get the string matching the given pattern.
|
|
1410
|
+
|
|
1411
|
+
Parameters
|
|
1412
|
+
----------
|
|
1413
|
+
pattern : str
|
|
1414
|
+
Regular expression pattern
|
|
1415
|
+
|
|
1416
|
+
Returns
|
|
1417
|
+
-------
|
|
1418
|
+
Stringable
|
|
1419
|
+
A new Stringable with the first match, or empty if no match
|
|
1420
|
+
"""
|
|
1421
|
+
match = re.search(pattern, str(self))
|
|
1422
|
+
return Stringable(match.group(0) if match else "")
|
|
1423
|
+
|
|
1424
|
+
def matchAll(self, pattern: str) -> List[str]:
|
|
1425
|
+
"""
|
|
1426
|
+
Get all strings matching the given pattern.
|
|
1427
|
+
|
|
1428
|
+
Parameters
|
|
1429
|
+
----------
|
|
1430
|
+
pattern : str
|
|
1431
|
+
Regular expression pattern
|
|
1432
|
+
|
|
1433
|
+
Returns
|
|
1434
|
+
-------
|
|
1435
|
+
list
|
|
1436
|
+
List of all matches
|
|
1437
|
+
"""
|
|
1438
|
+
return re.findall(pattern, str(self))
|
|
1439
|
+
|
|
1440
|
+
def isMatch(self, pattern: Union[str, List[str]]) -> bool:
|
|
1441
|
+
"""
|
|
1442
|
+
Determine if a given string matches a given pattern.
|
|
1443
|
+
|
|
1444
|
+
Parameters
|
|
1445
|
+
----------
|
|
1446
|
+
pattern : str or list
|
|
1447
|
+
Regular expression pattern(s)
|
|
1448
|
+
|
|
1449
|
+
Returns
|
|
1450
|
+
-------
|
|
1451
|
+
bool
|
|
1452
|
+
True if string matches pattern, False otherwise
|
|
1453
|
+
"""
|
|
1454
|
+
if isinstance(pattern, str):
|
|
1455
|
+
pattern = [pattern]
|
|
1456
|
+
|
|
1457
|
+
s = str(self)
|
|
1458
|
+
return any(re.search(p, s) is not None for p in pattern)
|
|
1459
|
+
|
|
1460
|
+
def test(self, pattern: str) -> bool:
|
|
1461
|
+
"""
|
|
1462
|
+
Determine if the string matches the given pattern.
|
|
1463
|
+
|
|
1464
|
+
Parameters
|
|
1465
|
+
----------
|
|
1466
|
+
pattern : str
|
|
1467
|
+
Regular expression pattern
|
|
1468
|
+
|
|
1469
|
+
Returns
|
|
1470
|
+
-------
|
|
1471
|
+
bool
|
|
1472
|
+
True if string matches pattern, False otherwise
|
|
1473
|
+
"""
|
|
1474
|
+
return self.isMatch(pattern)
|
|
1475
|
+
|
|
1476
|
+
def numbers(self) -> "Stringable":
|
|
1477
|
+
"""
|
|
1478
|
+
Remove all non-numeric characters from a string.
|
|
1479
|
+
|
|
1480
|
+
Returns
|
|
1481
|
+
-------
|
|
1482
|
+
Stringable
|
|
1483
|
+
A new Stringable with only numeric characters
|
|
1484
|
+
"""
|
|
1485
|
+
return Stringable(re.sub(r'\D', '', str(self)))
|
|
1486
|
+
|
|
1487
|
+
def excerpt(self, phrase: str = '', options: Optional[Dict] = None) -> Optional[str]:
|
|
1488
|
+
"""
|
|
1489
|
+
Extracts an excerpt from text that matches the first instance of a phrase.
|
|
1490
|
+
|
|
1491
|
+
Parameters
|
|
1492
|
+
----------
|
|
1493
|
+
phrase : str, optional
|
|
1494
|
+
The phrase to search for, by default ''
|
|
1495
|
+
options : dict, optional
|
|
1496
|
+
Options for excerpt extraction, by default None
|
|
1497
|
+
|
|
1498
|
+
Returns
|
|
1499
|
+
-------
|
|
1500
|
+
str or None
|
|
1501
|
+
The excerpt, or None if phrase not found
|
|
1502
|
+
"""
|
|
1503
|
+
if options is None:
|
|
1504
|
+
options = {}
|
|
1505
|
+
|
|
1506
|
+
radius = options.get('radius', 100)
|
|
1507
|
+
omission = options.get('omission', '...')
|
|
1508
|
+
|
|
1509
|
+
s = str(self)
|
|
1510
|
+
if not phrase:
|
|
1511
|
+
return s[:radius * 2] + (omission if len(s) > radius * 2 else '')
|
|
1512
|
+
|
|
1513
|
+
pos = s.lower().find(phrase.lower())
|
|
1514
|
+
if pos == -1:
|
|
1515
|
+
return None
|
|
1516
|
+
|
|
1517
|
+
start = max(0, pos - radius)
|
|
1518
|
+
end = min(len(s), pos + len(phrase) + radius)
|
|
1519
|
+
|
|
1520
|
+
excerpt = s[start:end]
|
|
1521
|
+
|
|
1522
|
+
if start > 0:
|
|
1523
|
+
excerpt = omission + excerpt
|
|
1524
|
+
if end < len(s):
|
|
1525
|
+
excerpt = excerpt + omission
|
|
1526
|
+
|
|
1527
|
+
return excerpt
|
|
1528
|
+
|
|
1529
|
+
def basename(self, suffix: str = '') -> "Stringable":
|
|
1530
|
+
"""
|
|
1531
|
+
Get the trailing name component of the path.
|
|
1532
|
+
|
|
1533
|
+
Parameters
|
|
1534
|
+
----------
|
|
1535
|
+
suffix : str, optional
|
|
1536
|
+
Suffix to remove, by default ''
|
|
1537
|
+
|
|
1538
|
+
Returns
|
|
1539
|
+
-------
|
|
1540
|
+
Stringable
|
|
1541
|
+
A new Stringable with the basename
|
|
1542
|
+
"""
|
|
1543
|
+
return Stringable(os.path.basename(str(self)).removesuffix(suffix))
|
|
1544
|
+
|
|
1545
|
+
def dirname(self, levels: int = 1) -> "Stringable":
|
|
1546
|
+
"""
|
|
1547
|
+
Get the parent directory's path.
|
|
1548
|
+
|
|
1549
|
+
Parameters
|
|
1550
|
+
----------
|
|
1551
|
+
levels : int, optional
|
|
1552
|
+
Number of levels up, by default 1
|
|
1553
|
+
|
|
1554
|
+
Returns
|
|
1555
|
+
-------
|
|
1556
|
+
Stringable
|
|
1557
|
+
A new Stringable with the directory name
|
|
1558
|
+
"""
|
|
1559
|
+
path = str(self)
|
|
1560
|
+
for _ in range(levels):
|
|
1561
|
+
path = os.path.dirname(path)
|
|
1562
|
+
return Stringable(path)
|
|
1563
|
+
|
|
1564
|
+
def classBasename(self) -> "Stringable":
|
|
1565
|
+
"""
|
|
1566
|
+
Get the basename of the class path.
|
|
1567
|
+
|
|
1568
|
+
Returns
|
|
1569
|
+
-------
|
|
1570
|
+
Stringable
|
|
1571
|
+
A new Stringable with the class basename
|
|
1572
|
+
"""
|
|
1573
|
+
# Extract the last part after the last dot (class name)
|
|
1574
|
+
parts = str(self).split('.')
|
|
1575
|
+
return Stringable(parts[-1] if parts else str(self))
|
|
1576
|
+
|
|
1577
|
+
def between(self, from_str: str, to_str: str) -> "Stringable":
|
|
1578
|
+
"""
|
|
1579
|
+
Get the portion of a string between two given values.
|
|
1580
|
+
|
|
1581
|
+
Parameters
|
|
1582
|
+
----------
|
|
1583
|
+
from_str : str
|
|
1584
|
+
Starting delimiter
|
|
1585
|
+
to_str : str
|
|
1586
|
+
Ending delimiter
|
|
1587
|
+
|
|
1588
|
+
Returns
|
|
1589
|
+
-------
|
|
1590
|
+
Stringable
|
|
1591
|
+
A new Stringable with the text between delimiters
|
|
1592
|
+
"""
|
|
1593
|
+
s = str(self)
|
|
1594
|
+
start = s.find(from_str)
|
|
1595
|
+
if start == -1:
|
|
1596
|
+
return Stringable("")
|
|
1597
|
+
|
|
1598
|
+
start += len(from_str)
|
|
1599
|
+
end = s.find(to_str, start)
|
|
1600
|
+
if end == -1:
|
|
1601
|
+
return Stringable("")
|
|
1602
|
+
|
|
1603
|
+
return Stringable(s[start:end])
|
|
1604
|
+
|
|
1605
|
+
def betweenFirst(self, from_str: str, to_str: str) -> "Stringable":
|
|
1606
|
+
"""
|
|
1607
|
+
Get the smallest possible portion of a string between two given values.
|
|
1608
|
+
|
|
1609
|
+
Parameters
|
|
1610
|
+
----------
|
|
1611
|
+
from_str : str
|
|
1612
|
+
Starting delimiter
|
|
1613
|
+
to_str : str
|
|
1614
|
+
Ending delimiter
|
|
1615
|
+
|
|
1616
|
+
Returns
|
|
1617
|
+
-------
|
|
1618
|
+
Stringable
|
|
1619
|
+
A new Stringable with the text between first delimiters
|
|
1620
|
+
"""
|
|
1621
|
+
s = str(self)
|
|
1622
|
+
start = s.find(from_str)
|
|
1623
|
+
if start == -1:
|
|
1624
|
+
return Stringable("")
|
|
1625
|
+
|
|
1626
|
+
start += len(from_str)
|
|
1627
|
+
end = s.find(to_str, start)
|
|
1628
|
+
if end == -1:
|
|
1629
|
+
return Stringable("")
|
|
1630
|
+
|
|
1631
|
+
return Stringable(s[start:end])
|
|
1632
|
+
|
|
1633
|
+
def finish(self, cap: str) -> "Stringable":
|
|
1634
|
+
"""
|
|
1635
|
+
Cap a string with a single instance of a given value.
|
|
1636
|
+
|
|
1637
|
+
Parameters
|
|
1638
|
+
----------
|
|
1639
|
+
cap : str
|
|
1640
|
+
The string to cap with
|
|
1641
|
+
|
|
1642
|
+
Returns
|
|
1643
|
+
-------
|
|
1644
|
+
Stringable
|
|
1645
|
+
A new Stringable that ends with the cap
|
|
1646
|
+
"""
|
|
1647
|
+
s = str(self)
|
|
1648
|
+
if not s.endswith(cap):
|
|
1649
|
+
s += cap
|
|
1650
|
+
return Stringable(s)
|
|
1651
|
+
|
|
1652
|
+
def start(self, prefix: str) -> "Stringable":
|
|
1653
|
+
"""
|
|
1654
|
+
Begin a string with a single instance of a given value.
|
|
1655
|
+
|
|
1656
|
+
Parameters
|
|
1657
|
+
----------
|
|
1658
|
+
prefix : str
|
|
1659
|
+
The string to start with
|
|
1660
|
+
|
|
1661
|
+
Returns
|
|
1662
|
+
-------
|
|
1663
|
+
Stringable
|
|
1664
|
+
A new Stringable that starts with the prefix
|
|
1665
|
+
"""
|
|
1666
|
+
s = str(self)
|
|
1667
|
+
if not s.startswith(prefix):
|
|
1668
|
+
s = prefix + s
|
|
1669
|
+
return Stringable(s)
|
|
1670
|
+
|
|
1671
|
+
def explode(self, delimiter: str, limit: int = -1) -> List[str]:
|
|
1672
|
+
"""
|
|
1673
|
+
Explode the string into a list using a delimiter.
|
|
1674
|
+
|
|
1675
|
+
Splits the string by the specified delimiter and returns a list of substrings.
|
|
1676
|
+
If a limit is specified, the string will be split into at most that many parts.
|
|
1677
|
+
|
|
1678
|
+
Parameters
|
|
1679
|
+
----------
|
|
1680
|
+
delimiter : str
|
|
1681
|
+
The delimiter to split on.
|
|
1682
|
+
limit : int, optional
|
|
1683
|
+
Maximum number of elements to return, by default -1 (no limit).
|
|
1684
|
+
|
|
1685
|
+
Returns
|
|
1686
|
+
-------
|
|
1687
|
+
list
|
|
1688
|
+
List of string parts after splitting by the delimiter.
|
|
1689
|
+
"""
|
|
1690
|
+
|
|
1691
|
+
# Check if limit is specified to control the number of splits
|
|
1692
|
+
if limit == -1:
|
|
1693
|
+
# Split without limit - all occurrences of delimiter are used
|
|
1694
|
+
return str(self).split(delimiter)
|
|
1695
|
+
else:
|
|
1696
|
+
# Split with limit - maximum of (limit-1) splits are performed
|
|
1697
|
+
return str(self).split(delimiter, limit - 1)
|
|
1698
|
+
|
|
1699
|
+
def split(self, pattern: Union[str, int], limit: int = -1, flags: int = 0) -> List[str]:
|
|
1700
|
+
"""
|
|
1701
|
+
Split a string using a regular expression or by length.
|
|
1702
|
+
|
|
1703
|
+
Parameters
|
|
1704
|
+
----------
|
|
1705
|
+
pattern : str or int
|
|
1706
|
+
Regular expression pattern or length for splitting
|
|
1707
|
+
limit : int, optional
|
|
1708
|
+
Maximum splits, by default -1 (no limit)
|
|
1709
|
+
flags : int, optional
|
|
1710
|
+
Regex flags, by default 0
|
|
1711
|
+
|
|
1712
|
+
Returns
|
|
1713
|
+
-------
|
|
1714
|
+
list
|
|
1715
|
+
List of string segments
|
|
1716
|
+
"""
|
|
1717
|
+
if isinstance(pattern, int):
|
|
1718
|
+
# Split by length
|
|
1719
|
+
s = str(self)
|
|
1720
|
+
return [s[i:i+pattern] for i in range(0, len(s), pattern)]
|
|
1721
|
+
else:
|
|
1722
|
+
# Split by regex
|
|
1723
|
+
# In re.split, maxsplit=0 means no limit, -1 means no splits
|
|
1724
|
+
maxsplit = 0 if limit == -1 else limit
|
|
1725
|
+
segments = re.split(pattern, str(self), maxsplit=maxsplit, flags=flags)
|
|
1726
|
+
return segments if segments else []
|
|
1727
|
+
|
|
1728
|
+
def ucsplit(self) -> List[str]:
|
|
1729
|
+
"""
|
|
1730
|
+
Split a string by uppercase characters.
|
|
1731
|
+
|
|
1732
|
+
Returns
|
|
1733
|
+
-------
|
|
1734
|
+
list
|
|
1735
|
+
List of words split by uppercase characters
|
|
1736
|
+
"""
|
|
1737
|
+
# Split on uppercase letters, keeping the uppercase letter with the following text
|
|
1738
|
+
parts = re.findall(r'[A-Z][a-z]*|[a-z]+|\d+', str(self))
|
|
1739
|
+
return parts if parts else [str(self)]
|
|
1740
|
+
|
|
1741
|
+
def squish(self) -> "Stringable":
|
|
1742
|
+
"""
|
|
1743
|
+
Remove all "extra" blank space from the given string.
|
|
1744
|
+
|
|
1745
|
+
Returns
|
|
1746
|
+
-------
|
|
1747
|
+
Stringable
|
|
1748
|
+
A new Stringable with normalized whitespace
|
|
1749
|
+
"""
|
|
1750
|
+
# Replace multiple whitespace characters with single spaces and trim
|
|
1751
|
+
return Stringable(re.sub(r'\s+', ' ', str(self)).strip())
|
|
1752
|
+
|
|
1753
|
+
def words(self, words: int = 100, end: str = '...') -> "Stringable":
|
|
1754
|
+
"""
|
|
1755
|
+
Limit the number of words in a string.
|
|
1756
|
+
|
|
1757
|
+
Parameters
|
|
1758
|
+
----------
|
|
1759
|
+
words : int, optional
|
|
1760
|
+
Maximum number of words, by default 100
|
|
1761
|
+
end : str, optional
|
|
1762
|
+
String to append if truncated, by default '...'
|
|
1763
|
+
|
|
1764
|
+
Returns
|
|
1765
|
+
-------
|
|
1766
|
+
Stringable
|
|
1767
|
+
A new Stringable with limited words
|
|
1768
|
+
"""
|
|
1769
|
+
word_list = str(self).split()
|
|
1770
|
+
if len(word_list) <= words:
|
|
1771
|
+
return Stringable(str(self))
|
|
1772
|
+
|
|
1773
|
+
truncated = ' '.join(word_list[:words])
|
|
1774
|
+
return Stringable(truncated + end)
|
|
1775
|
+
|
|
1776
|
+
def wordCount(self, characters: Optional[str] = None) -> int:
|
|
1777
|
+
"""
|
|
1778
|
+
Get the number of words a string contains.
|
|
1779
|
+
|
|
1780
|
+
Parameters
|
|
1781
|
+
----------
|
|
1782
|
+
characters : str, optional
|
|
1783
|
+
Additional characters to consider as word separators, by default None
|
|
1784
|
+
|
|
1785
|
+
Returns
|
|
1786
|
+
-------
|
|
1787
|
+
int
|
|
1788
|
+
Number of words in the string
|
|
1789
|
+
"""
|
|
1790
|
+
s = str(self).strip()
|
|
1791
|
+
if not s:
|
|
1792
|
+
return 0
|
|
1793
|
+
|
|
1794
|
+
if characters:
|
|
1795
|
+
# Replace additional characters with spaces
|
|
1796
|
+
for char in characters:
|
|
1797
|
+
s = s.replace(char, ' ')
|
|
1798
|
+
|
|
1799
|
+
# Split by whitespace and count non-empty parts
|
|
1800
|
+
return len([word for word in s.split() if word])
|
|
1801
|
+
|
|
1802
|
+
def wordWrap(self, characters: int = 75, break_str: str = "\n", cut_long_words: bool = False) -> "Stringable":
|
|
1803
|
+
"""
|
|
1804
|
+
Wrap a string to a given number of characters.
|
|
1805
|
+
|
|
1806
|
+
Parameters
|
|
1807
|
+
----------
|
|
1808
|
+
characters : int, optional
|
|
1809
|
+
Line width, by default 75
|
|
1810
|
+
break_str : str, optional
|
|
1811
|
+
Line break string, by default "\\n"
|
|
1812
|
+
cut_long_words : bool, optional
|
|
1813
|
+
Whether to cut long words, by default False
|
|
1814
|
+
|
|
1815
|
+
Returns
|
|
1816
|
+
-------
|
|
1817
|
+
Stringable
|
|
1818
|
+
A new Stringable with wrapped text
|
|
1819
|
+
"""
|
|
1820
|
+
import textwrap
|
|
1821
|
+
|
|
1822
|
+
if cut_long_words:
|
|
1823
|
+
wrapped = textwrap.fill(str(self), width=characters, break_long_words=True,
|
|
1824
|
+
break_on_hyphens=True, expand_tabs=False)
|
|
1825
|
+
else:
|
|
1826
|
+
wrapped = textwrap.fill(str(self), width=characters, break_long_words=False,
|
|
1827
|
+
break_on_hyphens=True, expand_tabs=False)
|
|
1828
|
+
|
|
1829
|
+
return Stringable(wrapped.replace('\n', break_str))
|
|
1830
|
+
|
|
1831
|
+
def wrap(self, before: str, after: Optional[str] = None) -> "Stringable":
|
|
1832
|
+
"""
|
|
1833
|
+
Wrap the string with the given strings.
|
|
1834
|
+
|
|
1835
|
+
Parameters
|
|
1836
|
+
----------
|
|
1837
|
+
before : str
|
|
1838
|
+
String to prepend
|
|
1839
|
+
after : str, optional
|
|
1840
|
+
String to append, by default None (uses before)
|
|
1841
|
+
|
|
1842
|
+
Returns
|
|
1843
|
+
-------
|
|
1844
|
+
Stringable
|
|
1845
|
+
A new Stringable wrapped with the given strings
|
|
1846
|
+
"""
|
|
1847
|
+
if after is None:
|
|
1848
|
+
after = before
|
|
1849
|
+
return Stringable(before + str(self) + after)
|
|
1850
|
+
|
|
1851
|
+
def unwrap(self, before: str, after: Optional[str] = None) -> "Stringable":
|
|
1852
|
+
"""
|
|
1853
|
+
Unwrap the string with the given strings.
|
|
1854
|
+
|
|
1855
|
+
Parameters
|
|
1856
|
+
----------
|
|
1857
|
+
before : str
|
|
1858
|
+
String to remove from start
|
|
1859
|
+
after : str, optional
|
|
1860
|
+
String to remove from end, by default None (uses before)
|
|
1861
|
+
|
|
1862
|
+
Returns
|
|
1863
|
+
-------
|
|
1864
|
+
Stringable
|
|
1865
|
+
A new Stringable with wrapping removed
|
|
1866
|
+
"""
|
|
1867
|
+
if after is None:
|
|
1868
|
+
after = before
|
|
1869
|
+
|
|
1870
|
+
s = str(self)
|
|
1871
|
+
if s.startswith(before):
|
|
1872
|
+
s = s[len(before):]
|
|
1873
|
+
if s.endswith(after):
|
|
1874
|
+
s = s[:-len(after)]
|
|
1875
|
+
|
|
1876
|
+
return Stringable(s)
|
|
1877
|
+
|
|
1878
|
+
# Advanced replacement methods
|
|
1879
|
+
def replaceArray(self, search: str, replace: List[str]) -> "Stringable":
|
|
1880
|
+
"""
|
|
1881
|
+
Replace a given value in the string sequentially with an array.
|
|
1882
|
+
|
|
1883
|
+
Parameters
|
|
1884
|
+
----------
|
|
1885
|
+
search : str
|
|
1886
|
+
The string to search for
|
|
1887
|
+
replace : list
|
|
1888
|
+
List of replacement strings
|
|
1889
|
+
|
|
1890
|
+
Returns
|
|
1891
|
+
-------
|
|
1892
|
+
Stringable
|
|
1893
|
+
A new Stringable with sequential replacements
|
|
1894
|
+
"""
|
|
1895
|
+
s = str(self)
|
|
1896
|
+
replace_idx = 0
|
|
1897
|
+
|
|
1898
|
+
while search in s and replace_idx < len(replace):
|
|
1899
|
+
s = s.replace(search, str(replace[replace_idx]), 1)
|
|
1900
|
+
replace_idx += 1
|
|
1901
|
+
|
|
1902
|
+
return Stringable(s)
|
|
1903
|
+
|
|
1904
|
+
def replaceFirst(self, search: str, replace: str) -> "Stringable":
|
|
1905
|
+
"""
|
|
1906
|
+
Replace the first occurrence of a given value in the string.
|
|
1907
|
+
|
|
1908
|
+
Parameters
|
|
1909
|
+
----------
|
|
1910
|
+
search : str
|
|
1911
|
+
The string to search for
|
|
1912
|
+
replace : str
|
|
1913
|
+
The replacement string
|
|
1914
|
+
|
|
1915
|
+
Returns
|
|
1916
|
+
-------
|
|
1917
|
+
Stringable
|
|
1918
|
+
A new Stringable with first occurrence replaced
|
|
1919
|
+
"""
|
|
1920
|
+
return Stringable(str(self).replace(search, replace, 1))
|
|
1921
|
+
|
|
1922
|
+
def replaceLast(self, search: str, replace: str) -> "Stringable":
|
|
1923
|
+
"""
|
|
1924
|
+
Replace the last occurrence of a given value in the string.
|
|
1925
|
+
|
|
1926
|
+
Parameters
|
|
1927
|
+
----------
|
|
1928
|
+
search : str
|
|
1929
|
+
The string to search for
|
|
1930
|
+
replace : str
|
|
1931
|
+
The replacement string
|
|
1932
|
+
|
|
1933
|
+
Returns
|
|
1934
|
+
-------
|
|
1935
|
+
Stringable
|
|
1936
|
+
A new Stringable with last occurrence replaced
|
|
1937
|
+
"""
|
|
1938
|
+
s = str(self)
|
|
1939
|
+
idx = s.rfind(search)
|
|
1940
|
+
if idx != -1:
|
|
1941
|
+
s = s[:idx] + replace + s[idx + len(search):]
|
|
1942
|
+
return Stringable(s)
|
|
1943
|
+
|
|
1944
|
+
def replaceStart(self, search: str, replace: str) -> "Stringable":
|
|
1945
|
+
"""
|
|
1946
|
+
Replace the first occurrence of the given value if it appears at the start of the string.
|
|
1947
|
+
|
|
1948
|
+
Parameters
|
|
1949
|
+
----------
|
|
1950
|
+
search : str
|
|
1951
|
+
The string to search for at the start
|
|
1952
|
+
replace : str
|
|
1953
|
+
The replacement string
|
|
1954
|
+
|
|
1955
|
+
Returns
|
|
1956
|
+
-------
|
|
1957
|
+
Stringable
|
|
1958
|
+
A new Stringable with start replacement
|
|
1959
|
+
"""
|
|
1960
|
+
s = str(self)
|
|
1961
|
+
if s.startswith(search):
|
|
1962
|
+
s = replace + s[len(search):]
|
|
1963
|
+
return Stringable(s)
|
|
1964
|
+
|
|
1965
|
+
def replaceEnd(self, search: str, replace: str) -> "Stringable":
|
|
1966
|
+
"""
|
|
1967
|
+
Replace the last occurrence of a given value if it appears at the end of the string.
|
|
1968
|
+
|
|
1969
|
+
Parameters
|
|
1970
|
+
----------
|
|
1971
|
+
search : str
|
|
1972
|
+
The string to search for at the end
|
|
1973
|
+
replace : str
|
|
1974
|
+
The replacement string
|
|
1975
|
+
|
|
1976
|
+
Returns
|
|
1977
|
+
-------
|
|
1978
|
+
Stringable
|
|
1979
|
+
A new Stringable with end replacement
|
|
1980
|
+
"""
|
|
1981
|
+
s = str(self)
|
|
1982
|
+
if s.endswith(search):
|
|
1983
|
+
s = s[:-len(search)] + replace
|
|
1984
|
+
return Stringable(s)
|
|
1985
|
+
|
|
1986
|
+
def replaceMatches(self, pattern: Union[str, List[str]], replace: Union[str, Callable], limit: int = -1) -> "Stringable":
|
|
1987
|
+
"""
|
|
1988
|
+
Replace the patterns matching the given regular expression.
|
|
1989
|
+
|
|
1990
|
+
Parameters
|
|
1991
|
+
----------
|
|
1992
|
+
pattern : str or list
|
|
1993
|
+
Regular expression pattern(s)
|
|
1994
|
+
replace : str or callable
|
|
1995
|
+
Replacement string or callback function
|
|
1996
|
+
limit : int, optional
|
|
1997
|
+
Maximum replacements, by default -1 (no limit)
|
|
1998
|
+
|
|
1999
|
+
Returns
|
|
2000
|
+
-------
|
|
2001
|
+
Stringable
|
|
2002
|
+
A new Stringable with pattern matches replaced
|
|
2003
|
+
"""
|
|
2004
|
+
s = str(self)
|
|
2005
|
+
|
|
2006
|
+
if isinstance(pattern, list):
|
|
2007
|
+
patterns = pattern
|
|
2008
|
+
else:
|
|
2009
|
+
patterns = [pattern]
|
|
2010
|
+
|
|
2011
|
+
for pat in patterns:
|
|
2012
|
+
if callable(replace):
|
|
2013
|
+
s = re.sub(pat, replace, s, count=0 if limit == -1 else limit)
|
|
2014
|
+
else:
|
|
2015
|
+
s = re.sub(pat, str(replace), s, count=0 if limit == -1 else limit)
|
|
2016
|
+
|
|
2017
|
+
return Stringable(s)
|
|
2018
|
+
|
|
2019
|
+
def remove(self, search: Union[str, List[str]], case_sensitive: bool = True) -> "Stringable":
|
|
2020
|
+
"""
|
|
2021
|
+
Remove any occurrence of the given string in the subject.
|
|
2022
|
+
|
|
2023
|
+
Parameters
|
|
2024
|
+
----------
|
|
2025
|
+
search : str or list
|
|
2026
|
+
The string(s) to remove
|
|
2027
|
+
case_sensitive : bool, optional
|
|
2028
|
+
Whether the search is case sensitive, by default True
|
|
2029
|
+
|
|
2030
|
+
Returns
|
|
2031
|
+
-------
|
|
2032
|
+
Stringable
|
|
2033
|
+
A new Stringable with occurrences removed
|
|
2034
|
+
"""
|
|
2035
|
+
s = str(self)
|
|
2036
|
+
|
|
2037
|
+
if isinstance(search, str):
|
|
2038
|
+
search = [search]
|
|
2039
|
+
|
|
2040
|
+
for needle in search:
|
|
2041
|
+
if case_sensitive:
|
|
2042
|
+
s = s.replace(needle, '')
|
|
2043
|
+
else:
|
|
2044
|
+
s = re.sub(re.escape(needle), '', s, flags=re.IGNORECASE)
|
|
2045
|
+
|
|
2046
|
+
return Stringable(s)
|
|
2047
|
+
|
|
2048
|
+
# Pluralization and singularization methods
|
|
2049
|
+
def plural(self, count: Union[int, List, Any] = 2, prepend_count: bool = False) -> "Stringable":
|
|
2050
|
+
"""
|
|
2051
|
+
Get the plural form of an English word.
|
|
2052
|
+
|
|
2053
|
+
Parameters
|
|
2054
|
+
----------
|
|
2055
|
+
count : int, list or any, optional
|
|
2056
|
+
Count to determine if plural is needed, by default 2
|
|
2057
|
+
prepend_count : bool, optional
|
|
2058
|
+
Whether to prepend the count, by default False
|
|
2059
|
+
|
|
2060
|
+
Returns
|
|
2061
|
+
-------
|
|
2062
|
+
Stringable
|
|
2063
|
+
A new Stringable with plural form
|
|
2064
|
+
"""
|
|
2065
|
+
# Simple pluralization rules
|
|
2066
|
+
word = str(self).lower()
|
|
2067
|
+
|
|
2068
|
+
# Determine if we need plural
|
|
2069
|
+
if hasattr(count, '__len__'):
|
|
2070
|
+
actual_count = len(count)
|
|
2071
|
+
elif isinstance(count, (int, float)):
|
|
2072
|
+
actual_count = count
|
|
2073
|
+
else:
|
|
2074
|
+
actual_count = 1
|
|
2075
|
+
|
|
2076
|
+
if actual_count == 1:
|
|
2077
|
+
result = str(self)
|
|
2078
|
+
else:
|
|
2079
|
+
# Simple pluralization rules
|
|
2080
|
+
if word.endswith(('s', 'sh', 'ch', 'x', 'z')):
|
|
2081
|
+
plural_word = str(self) + 'es'
|
|
2082
|
+
elif word.endswith('y') and len(word) > 1 and word[-2] not in 'aeiou':
|
|
2083
|
+
plural_word = str(self)[:-1] + 'ies'
|
|
2084
|
+
elif word.endswith('f'):
|
|
2085
|
+
plural_word = str(self)[:-1] + 'ves'
|
|
2086
|
+
elif word.endswith('fe'):
|
|
2087
|
+
plural_word = str(self)[:-2] + 'ves'
|
|
2088
|
+
else:
|
|
2089
|
+
plural_word = str(self) + 's'
|
|
2090
|
+
|
|
2091
|
+
result = plural_word
|
|
2092
|
+
|
|
2093
|
+
if prepend_count:
|
|
2094
|
+
result = f"{actual_count} {result}"
|
|
2095
|
+
|
|
2096
|
+
return Stringable(result)
|
|
2097
|
+
|
|
2098
|
+
def pluralStudly(self, count: Union[int, List, Any] = 2) -> "Stringable":
|
|
2099
|
+
"""
|
|
2100
|
+
Pluralize the last word of an English, studly caps case string.
|
|
2101
|
+
|
|
2102
|
+
Parameters
|
|
2103
|
+
----------
|
|
2104
|
+
count : int, list or any, optional
|
|
2105
|
+
Count to determine if plural is needed, by default 2
|
|
2106
|
+
|
|
2107
|
+
Returns
|
|
2108
|
+
-------
|
|
2109
|
+
Stringable
|
|
2110
|
+
A new Stringable with pluralized last word in StudlyCase
|
|
2111
|
+
"""
|
|
2112
|
+
s = str(self)
|
|
2113
|
+
# Find the last word boundary
|
|
2114
|
+
parts = re.findall(r'[A-Z][a-z]*|[a-z]+', s)
|
|
2115
|
+
if parts:
|
|
2116
|
+
last_word = parts[-1]
|
|
2117
|
+
pluralized_last = Stringable(last_word).plural(count).studly().value()
|
|
2118
|
+
parts[-1] = pluralized_last
|
|
2119
|
+
return Stringable(''.join(parts))
|
|
2120
|
+
|
|
2121
|
+
return self.plural(count).studly()
|
|
2122
|
+
|
|
2123
|
+
def pluralPascal(self, count: Union[int, List, Any] = 2) -> "Stringable":
|
|
2124
|
+
"""
|
|
2125
|
+
Pluralize the last word of an English, Pascal caps case string.
|
|
2126
|
+
|
|
2127
|
+
Parameters
|
|
2128
|
+
----------
|
|
2129
|
+
count : int, list or any, optional
|
|
2130
|
+
Count to determine if plural is needed, by default 2
|
|
2131
|
+
|
|
2132
|
+
Returns
|
|
2133
|
+
-------
|
|
2134
|
+
Stringable
|
|
2135
|
+
A new Stringable with pluralized last word in PascalCase
|
|
2136
|
+
"""
|
|
2137
|
+
# PascalCase is the same as StudlyCase
|
|
2138
|
+
s = str(self)
|
|
2139
|
+
if len(s) == 0:
|
|
2140
|
+
return Stringable(s)
|
|
2141
|
+
|
|
2142
|
+
# Split by uppercase letters to find words
|
|
2143
|
+
words = re.findall(r'[A-Z][a-z]*|[a-z]+', s)
|
|
2144
|
+
if not words:
|
|
2145
|
+
return Stringable(s)
|
|
2146
|
+
|
|
2147
|
+
# Determine if we need plural
|
|
2148
|
+
if isinstance(count, (list, tuple)):
|
|
2149
|
+
need_plural = len(count) != 1
|
|
2150
|
+
else:
|
|
2151
|
+
need_plural = count != 1
|
|
2152
|
+
|
|
2153
|
+
if need_plural:
|
|
2154
|
+
# Pluralize the last word
|
|
2155
|
+
last_word = words[-1]
|
|
2156
|
+
pluralized = Stringable(last_word).plural(count)
|
|
2157
|
+
words[-1] = pluralized.studly().value()
|
|
2158
|
+
|
|
2159
|
+
return Stringable(''.join(words))
|
|
2160
|
+
|
|
2161
|
+
def singular(self) -> "Stringable":
|
|
2162
|
+
"""
|
|
2163
|
+
Get the singular form of an English word.
|
|
2164
|
+
|
|
2165
|
+
Returns
|
|
2166
|
+
-------
|
|
2167
|
+
Stringable
|
|
2168
|
+
A new Stringable with singular form
|
|
2169
|
+
"""
|
|
2170
|
+
word = str(self).lower()
|
|
2171
|
+
s = str(self)
|
|
2172
|
+
|
|
2173
|
+
# Simple singularization rules
|
|
2174
|
+
if word.endswith('ies') and len(word) > 3:
|
|
2175
|
+
result = s[:-3] + 'y'
|
|
2176
|
+
elif word.endswith('ves'):
|
|
2177
|
+
if word.endswith('ives'):
|
|
2178
|
+
result = s[:-3] + 'e'
|
|
2179
|
+
else:
|
|
2180
|
+
result = s[:-3] + 'f'
|
|
2181
|
+
elif word.endswith('es'):
|
|
2182
|
+
if word.endswith(('ches', 'shes', 'xes', 'zes')):
|
|
2183
|
+
result = s[:-2]
|
|
2184
|
+
elif word.endswith('ses'):
|
|
2185
|
+
result = s[:-2]
|
|
2186
|
+
else:
|
|
2187
|
+
result = s[:-1]
|
|
2188
|
+
elif word.endswith('s') and not word.endswith('ss'):
|
|
2189
|
+
result = s[:-1]
|
|
2190
|
+
else:
|
|
2191
|
+
result = s
|
|
2192
|
+
|
|
2193
|
+
return Stringable(result)
|
|
2194
|
+
|
|
2195
|
+
def parseCallback(self, default: Optional[str] = None) -> List[Optional[str]]:
|
|
2196
|
+
"""
|
|
2197
|
+
Parse a Class@method style callback into class and method.
|
|
2198
|
+
|
|
2199
|
+
Parameters
|
|
2200
|
+
----------
|
|
2201
|
+
default : str, optional
|
|
2202
|
+
Default method name if not specified, by default None
|
|
2203
|
+
|
|
2204
|
+
Returns
|
|
2205
|
+
-------
|
|
2206
|
+
list
|
|
2207
|
+
List containing [class_name, method_name]
|
|
2208
|
+
"""
|
|
2209
|
+
callback_str = str(self)
|
|
2210
|
+
|
|
2211
|
+
if '@' in callback_str:
|
|
2212
|
+
parts = callback_str.split('@', 1)
|
|
2213
|
+
return [parts[0], parts[1]]
|
|
2214
|
+
else:
|
|
2215
|
+
return [callback_str, default]
|
|
2216
|
+
|
|
2217
|
+
def when(self, condition: Union[bool, Callable], callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2218
|
+
"""
|
|
2219
|
+
Execute the given callback if condition is true.
|
|
2220
|
+
|
|
2221
|
+
Parameters
|
|
2222
|
+
----------
|
|
2223
|
+
condition : bool or callable
|
|
2224
|
+
The condition to evaluate
|
|
2225
|
+
callback : callable
|
|
2226
|
+
The callback to execute if condition is true
|
|
2227
|
+
default : callable, optional
|
|
2228
|
+
The callback to execute if condition is false, by default None
|
|
2229
|
+
|
|
2230
|
+
Returns
|
|
2231
|
+
-------
|
|
2232
|
+
Stringable
|
|
2233
|
+
Result of callback execution or self
|
|
2234
|
+
"""
|
|
2235
|
+
if callable(condition):
|
|
2236
|
+
condition_result = condition(self)
|
|
2237
|
+
else:
|
|
2238
|
+
condition_result = condition
|
|
2239
|
+
|
|
2240
|
+
if condition_result:
|
|
2241
|
+
result = callback(self)
|
|
2242
|
+
return Stringable(result) if not isinstance(result, Stringable) else result
|
|
2243
|
+
elif default:
|
|
2244
|
+
result = default(self)
|
|
2245
|
+
return Stringable(result) if not isinstance(result, Stringable) else result
|
|
2246
|
+
else:
|
|
2247
|
+
return self
|
|
2248
|
+
|
|
2249
|
+
def whenContains(self, needles: Union[str, List[str]], callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2250
|
+
"""
|
|
2251
|
+
Execute the given callback if the string contains a given substring.
|
|
2252
|
+
|
|
2253
|
+
Parameters
|
|
2254
|
+
----------
|
|
2255
|
+
needles : str or list
|
|
2256
|
+
The substring(s) to search for
|
|
2257
|
+
callback : callable
|
|
2258
|
+
The callback to execute if condition is true
|
|
2259
|
+
default : callable, optional
|
|
2260
|
+
The callback to execute if condition is false, by default None
|
|
2261
|
+
|
|
2262
|
+
Returns
|
|
2263
|
+
-------
|
|
2264
|
+
Stringable
|
|
2265
|
+
Result of callback execution or self
|
|
2266
|
+
"""
|
|
2267
|
+
return self.when(self.contains(needles), callback, default)
|
|
2268
|
+
|
|
2269
|
+
def whenContainsAll(self, needles: List[str], callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2270
|
+
"""
|
|
2271
|
+
Execute the given callback if the string contains all array values.
|
|
2272
|
+
|
|
2273
|
+
Parameters
|
|
2274
|
+
----------
|
|
2275
|
+
needles : list
|
|
2276
|
+
The substrings to search for
|
|
2277
|
+
callback : callable
|
|
2278
|
+
The callback to execute if condition is true
|
|
2279
|
+
default : callable, optional
|
|
2280
|
+
The callback to execute if condition is false, by default None
|
|
2281
|
+
|
|
2282
|
+
Returns
|
|
2283
|
+
-------
|
|
2284
|
+
Stringable
|
|
2285
|
+
Result of callback execution or self
|
|
2286
|
+
"""
|
|
2287
|
+
contains_all = all(needle in str(self) for needle in needles)
|
|
2288
|
+
return self.when(contains_all, callback, default)
|
|
2289
|
+
|
|
2290
|
+
def whenEmpty(self, callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2291
|
+
"""
|
|
2292
|
+
Execute the given callback if the string is empty.
|
|
2293
|
+
|
|
2294
|
+
Parameters
|
|
2295
|
+
----------
|
|
2296
|
+
callback : callable
|
|
2297
|
+
The callback to execute if condition is true
|
|
2298
|
+
default : callable, optional
|
|
2299
|
+
The callback to execute if condition is false, by default None
|
|
2300
|
+
|
|
2301
|
+
Returns
|
|
2302
|
+
-------
|
|
2303
|
+
Stringable
|
|
2304
|
+
Result of callback execution or self
|
|
2305
|
+
"""
|
|
2306
|
+
return self.when(self.isEmpty(), callback, default)
|
|
2307
|
+
|
|
2308
|
+
def whenNotEmpty(self, callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2309
|
+
"""
|
|
2310
|
+
Execute the given callback if the string is not empty.
|
|
2311
|
+
|
|
2312
|
+
Parameters
|
|
2313
|
+
----------
|
|
2314
|
+
callback : callable
|
|
2315
|
+
The callback to execute if condition is true
|
|
2316
|
+
default : callable, optional
|
|
2317
|
+
The callback to execute if condition is false, by default None
|
|
2318
|
+
|
|
2319
|
+
Returns
|
|
2320
|
+
-------
|
|
2321
|
+
Stringable
|
|
2322
|
+
Result of callback execution or self
|
|
2323
|
+
"""
|
|
2324
|
+
return self.when(self.isNotEmpty(), callback, default)
|
|
2325
|
+
|
|
2326
|
+
def whenEndsWith(self, needles: Union[str, List[str]], callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2327
|
+
"""
|
|
2328
|
+
Execute the given callback if the string ends with a given substring.
|
|
2329
|
+
|
|
2330
|
+
Parameters
|
|
2331
|
+
----------
|
|
2332
|
+
needles : str or list
|
|
2333
|
+
The substring(s) to check
|
|
2334
|
+
callback : callable
|
|
2335
|
+
The callback to execute if condition is true
|
|
2336
|
+
default : callable, optional
|
|
2337
|
+
The callback to execute if condition is false, by default None
|
|
2338
|
+
|
|
2339
|
+
Returns
|
|
2340
|
+
-------
|
|
2341
|
+
Stringable
|
|
2342
|
+
Result of callback execution or self
|
|
2343
|
+
"""
|
|
2344
|
+
return self.when(self.endsWith(needles), callback, default)
|
|
2345
|
+
|
|
2346
|
+
def whenDoesntEndWith(self, needles: Union[str, List[str]], callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2347
|
+
"""
|
|
2348
|
+
Execute the given callback if the string doesn't end with a given substring.
|
|
2349
|
+
|
|
2350
|
+
Parameters
|
|
2351
|
+
----------
|
|
2352
|
+
needles : str or list
|
|
2353
|
+
The substring(s) to check
|
|
2354
|
+
callback : callable
|
|
2355
|
+
The callback to execute if condition is true
|
|
2356
|
+
default : callable, optional
|
|
2357
|
+
The callback to execute if condition is false, by default None
|
|
2358
|
+
|
|
2359
|
+
Returns
|
|
2360
|
+
-------
|
|
2361
|
+
Stringable
|
|
2362
|
+
Result of callback execution or self
|
|
2363
|
+
"""
|
|
2364
|
+
return self.when(not self.endsWith(needles), callback, default)
|
|
2365
|
+
|
|
2366
|
+
def whenExactly(self, value: str, callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2367
|
+
"""
|
|
2368
|
+
Execute the given callback if the string is an exact match with the given value.
|
|
2369
|
+
|
|
2370
|
+
Parameters
|
|
2371
|
+
----------
|
|
2372
|
+
value : str
|
|
2373
|
+
The value to compare exactly
|
|
2374
|
+
callback : callable
|
|
2375
|
+
The callback to execute if condition is true
|
|
2376
|
+
default : callable, optional
|
|
2377
|
+
The callback to execute if condition is false, by default None
|
|
2378
|
+
|
|
2379
|
+
Returns
|
|
2380
|
+
-------
|
|
2381
|
+
Stringable
|
|
2382
|
+
Result of callback execution or self
|
|
2383
|
+
"""
|
|
2384
|
+
return self.when(self.exactly(value), callback, default)
|
|
2385
|
+
|
|
2386
|
+
def whenNotExactly(self, value: str, callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2387
|
+
"""
|
|
2388
|
+
Execute the given callback if the string is not an exact match with the given value.
|
|
2389
|
+
|
|
2390
|
+
Parameters
|
|
2391
|
+
----------
|
|
2392
|
+
value : str
|
|
2393
|
+
The value to compare exactly
|
|
2394
|
+
callback : callable
|
|
2395
|
+
The callback to execute if condition is true
|
|
2396
|
+
default : callable, optional
|
|
2397
|
+
The callback to execute if condition is false, by default None
|
|
2398
|
+
|
|
2399
|
+
Returns
|
|
2400
|
+
-------
|
|
2401
|
+
Stringable
|
|
2402
|
+
Result of callback execution or self
|
|
2403
|
+
"""
|
|
2404
|
+
return self.when(not self.exactly(value), callback, default)
|
|
2405
|
+
|
|
2406
|
+
def whenStartsWith(self, needles: Union[str, List[str]], callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2407
|
+
"""
|
|
2408
|
+
Execute the given callback if the string starts with a given substring.
|
|
2409
|
+
|
|
2410
|
+
Parameters
|
|
2411
|
+
----------
|
|
2412
|
+
needles : str or list
|
|
2413
|
+
The substring(s) to check
|
|
2414
|
+
callback : callable
|
|
2415
|
+
The callback to execute if condition is true
|
|
2416
|
+
default : callable, optional
|
|
2417
|
+
The callback to execute if condition is false, by default None
|
|
2418
|
+
|
|
2419
|
+
Returns
|
|
2420
|
+
-------
|
|
2421
|
+
Stringable
|
|
2422
|
+
Result of callback execution or self
|
|
2423
|
+
"""
|
|
2424
|
+
if isinstance(needles, str):
|
|
2425
|
+
needles = [needles]
|
|
2426
|
+
starts_with = any(str(self).startswith(needle) for needle in needles)
|
|
2427
|
+
return self.when(starts_with, callback, default)
|
|
2428
|
+
|
|
2429
|
+
def whenDoesntStartWith(self, needles: Union[str, List[str]], callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2430
|
+
"""
|
|
2431
|
+
Execute the given callback if the string doesn't start with a given substring.
|
|
2432
|
+
|
|
2433
|
+
Parameters
|
|
2434
|
+
----------
|
|
2435
|
+
needles : str or list
|
|
2436
|
+
The substring(s) to check
|
|
2437
|
+
callback : callable
|
|
2438
|
+
The callback to execute if condition is true
|
|
2439
|
+
default : callable, optional
|
|
2440
|
+
The callback to execute if condition is false, by default None
|
|
2441
|
+
|
|
2442
|
+
Returns
|
|
2443
|
+
-------
|
|
2444
|
+
Stringable
|
|
2445
|
+
Result of callback execution or self
|
|
2446
|
+
"""
|
|
2447
|
+
if isinstance(needles, str):
|
|
2448
|
+
needles = [needles]
|
|
2449
|
+
starts_with = any(str(self).startswith(needle) for needle in needles)
|
|
2450
|
+
return self.when(not starts_with, callback, default)
|
|
2451
|
+
|
|
2452
|
+
def whenTest(self, pattern: str, callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2453
|
+
"""
|
|
2454
|
+
Execute the given callback if the string matches the given pattern.
|
|
2455
|
+
|
|
2456
|
+
Parameters
|
|
2457
|
+
----------
|
|
2458
|
+
pattern : str
|
|
2459
|
+
Regular expression pattern
|
|
2460
|
+
callback : callable
|
|
2461
|
+
The callback to execute if condition is true
|
|
2462
|
+
default : callable, optional
|
|
2463
|
+
The callback to execute if condition is false, by default None
|
|
2464
|
+
|
|
2465
|
+
Returns
|
|
2466
|
+
-------
|
|
2467
|
+
Stringable
|
|
2468
|
+
Result of callback execution or self
|
|
2469
|
+
"""
|
|
2470
|
+
return self.when(self.test(pattern), callback, default)
|
|
2471
|
+
|
|
2472
|
+
def convertCase(self, mode: int = None) -> "Stringable":
|
|
2473
|
+
"""
|
|
2474
|
+
Convert the case of a string.
|
|
2475
|
+
|
|
2476
|
+
Parameters
|
|
2477
|
+
----------
|
|
2478
|
+
mode : int, optional
|
|
2479
|
+
Case conversion mode:
|
|
2480
|
+
0 or None - MB_CASE_FOLD (casefold)
|
|
2481
|
+
1 - MB_CASE_UPPER (upper)
|
|
2482
|
+
2 - MB_CASE_LOWER (lower)
|
|
2483
|
+
3 - MB_CASE_TITLE (title)
|
|
2484
|
+
by default None (MB_CASE_FOLD)
|
|
2485
|
+
Returns
|
|
2486
|
+
-------
|
|
2487
|
+
Stringable
|
|
2488
|
+
A new Stringable with converted case
|
|
2489
|
+
"""
|
|
2490
|
+
s = str(self)
|
|
2491
|
+
|
|
2492
|
+
# Python doesn't have exact MB_CASE constants, so we'll use simple mappings
|
|
2493
|
+
if mode is None or mode == 0: # MB_CASE_FOLD equivalent
|
|
2494
|
+
return Stringable(s.casefold())
|
|
2495
|
+
elif mode == 1: # MB_CASE_UPPER equivalent
|
|
2496
|
+
return Stringable(s.upper())
|
|
2497
|
+
elif mode == 2: # MB_CASE_LOWER equivalent
|
|
2498
|
+
return Stringable(s.lower())
|
|
2499
|
+
elif mode == 3: # MB_CASE_TITLE equivalent
|
|
2500
|
+
return Stringable(s.title())
|
|
2501
|
+
else:
|
|
2502
|
+
return Stringable(s.casefold())
|
|
2503
|
+
|
|
2504
|
+
def transliterate(self, unknown: str = '?', strict: bool = False) -> "Stringable":
|
|
2505
|
+
"""
|
|
2506
|
+
Transliterate a string to its closest ASCII representation.
|
|
2507
|
+
|
|
2508
|
+
Parameters
|
|
2509
|
+
----------
|
|
2510
|
+
unknown : str, optional
|
|
2511
|
+
Character to use for unknown characters, by default '?'
|
|
2512
|
+
strict : bool, optional
|
|
2513
|
+
Whether to be strict about transliteration, by default False
|
|
2514
|
+
|
|
2515
|
+
Returns
|
|
2516
|
+
-------
|
|
2517
|
+
Stringable
|
|
2518
|
+
A new Stringable with transliterated text
|
|
2519
|
+
"""
|
|
2520
|
+
s = str(self)
|
|
2521
|
+
|
|
2522
|
+
# Use unicodedata to normalize and transliterate
|
|
2523
|
+
normalized = unicodedata.normalize('NFKD', s)
|
|
2524
|
+
|
|
2525
|
+
if strict:
|
|
2526
|
+
# Only keep ASCII characters
|
|
2527
|
+
ascii_chars = []
|
|
2528
|
+
for char in normalized:
|
|
2529
|
+
if ord(char) < 128:
|
|
2530
|
+
ascii_chars.append(char)
|
|
2531
|
+
else:
|
|
2532
|
+
ascii_chars.append(unknown)
|
|
2533
|
+
return Stringable(''.join(ascii_chars))
|
|
2534
|
+
else:
|
|
2535
|
+
# More lenient transliteration
|
|
2536
|
+
ascii_str = ''.join(char for char in normalized if ord(char) < 128)
|
|
2537
|
+
return Stringable(ascii_str)
|
|
2538
|
+
|
|
2539
|
+
def hash(self, algorithm: str) -> "Stringable":
|
|
2540
|
+
"""
|
|
2541
|
+
Hash the string using the given algorithm.
|
|
2542
|
+
|
|
2543
|
+
Parameters
|
|
2544
|
+
----------
|
|
2545
|
+
algorithm : str
|
|
2546
|
+
Hash algorithm (md5, sha1, sha256, etc.)
|
|
2547
|
+
|
|
2548
|
+
Returns
|
|
2549
|
+
-------
|
|
2550
|
+
Stringable
|
|
2551
|
+
A new Stringable with the hash
|
|
2552
|
+
"""
|
|
2553
|
+
hash_obj = hashlib.new(algorithm)
|
|
2554
|
+
hash_obj.update(str(self).encode('utf-8'))
|
|
2555
|
+
return Stringable(hash_obj.hexdigest())
|
|
2556
|
+
|
|
2557
|
+
def pipe(self, callback: Callable) -> "Stringable":
|
|
2558
|
+
"""
|
|
2559
|
+
Call the given callback and return a new string.
|
|
2560
|
+
|
|
2561
|
+
Parameters
|
|
2562
|
+
----------
|
|
2563
|
+
callback : callable
|
|
2564
|
+
The callback function to apply
|
|
2565
|
+
|
|
2566
|
+
Returns
|
|
2567
|
+
-------
|
|
2568
|
+
Stringable
|
|
2569
|
+
A new Stringable with the result of the callback
|
|
2570
|
+
"""
|
|
2571
|
+
result = callback(self)
|
|
2572
|
+
return Stringable(result) if not isinstance(result, Stringable) else result
|
|
2573
|
+
|
|
2574
|
+
def take(self, limit: int) -> "Stringable":
|
|
2575
|
+
"""
|
|
2576
|
+
Take the first or last {limit} characters.
|
|
2577
|
+
|
|
2578
|
+
Parameters
|
|
2579
|
+
----------
|
|
2580
|
+
limit : int
|
|
2581
|
+
Number of characters to take (negative for from end)
|
|
2582
|
+
|
|
2583
|
+
Returns
|
|
2584
|
+
-------
|
|
2585
|
+
Stringable
|
|
2586
|
+
A new Stringable with the taken characters
|
|
2587
|
+
"""
|
|
2588
|
+
if limit < 0:
|
|
2589
|
+
return Stringable(str(self)[limit:])
|
|
2590
|
+
else:
|
|
2591
|
+
return Stringable(str(self)[:limit])
|
|
2592
|
+
|
|
2593
|
+
def swap(self, map_dict: Dict[str, str]) -> "Stringable":
|
|
2594
|
+
"""
|
|
2595
|
+
Swap multiple keywords in a string with other keywords.
|
|
2596
|
+
|
|
2597
|
+
Parameters
|
|
2598
|
+
----------
|
|
2599
|
+
map_dict : dict
|
|
2600
|
+
Dictionary mapping old values to new values
|
|
2601
|
+
|
|
2602
|
+
Returns
|
|
2603
|
+
-------
|
|
2604
|
+
Stringable
|
|
2605
|
+
A new Stringable with swapped values
|
|
2606
|
+
"""
|
|
2607
|
+
s = str(self)
|
|
2608
|
+
for old, new in map_dict.items():
|
|
2609
|
+
s = s.replace(old, new)
|
|
2610
|
+
return Stringable(s)
|
|
2611
|
+
|
|
2612
|
+
def substrCount(self, needle: str, offset: int = 0, length: Optional[int] = None) -> int:
|
|
2613
|
+
"""
|
|
2614
|
+
Returns the number of substring occurrences.
|
|
2615
|
+
|
|
2616
|
+
Parameters
|
|
2617
|
+
----------
|
|
2618
|
+
needle : str
|
|
2619
|
+
The substring to count
|
|
2620
|
+
offset : int, optional
|
|
2621
|
+
Starting offset, by default 0
|
|
2622
|
+
length : int, optional
|
|
2623
|
+
Length to search within, by default None
|
|
2624
|
+
|
|
2625
|
+
Returns
|
|
2626
|
+
-------
|
|
2627
|
+
int
|
|
2628
|
+
Number of occurrences
|
|
2629
|
+
"""
|
|
2630
|
+
s = str(self)
|
|
2631
|
+
|
|
2632
|
+
if length is not None:
|
|
2633
|
+
s = s[offset:offset + length]
|
|
2634
|
+
else:
|
|
2635
|
+
s = s[offset:]
|
|
2636
|
+
|
|
2637
|
+
return s.count(needle)
|
|
2638
|
+
|
|
2639
|
+
def substrReplace(self, replace: Union[str, List[str]], offset: Union[int, List[int]] = 0,
|
|
2640
|
+
length: Optional[Union[int, List[int]]] = None) -> "Stringable":
|
|
2641
|
+
"""
|
|
2642
|
+
Replace text within a portion of a string.
|
|
2643
|
+
|
|
2644
|
+
Parameters
|
|
2645
|
+
----------
|
|
2646
|
+
replace : str or list
|
|
2647
|
+
Replacement string(s)
|
|
2648
|
+
offset : int or list, optional
|
|
2649
|
+
Starting position(s), by default 0
|
|
2650
|
+
length : int, list or None, optional
|
|
2651
|
+
Length(s) to replace, by default None
|
|
2652
|
+
|
|
2653
|
+
Returns
|
|
2654
|
+
-------
|
|
2655
|
+
Stringable
|
|
2656
|
+
A new Stringable with replaced text
|
|
2657
|
+
"""
|
|
2658
|
+
s = str(self)
|
|
2659
|
+
|
|
2660
|
+
if isinstance(replace, str):
|
|
2661
|
+
replace = [replace]
|
|
2662
|
+
if isinstance(offset, int):
|
|
2663
|
+
offset = [offset]
|
|
2664
|
+
if length is not None and isinstance(length, int):
|
|
2665
|
+
length = [length]
|
|
2666
|
+
|
|
2667
|
+
# Process replacements
|
|
2668
|
+
result = s
|
|
2669
|
+
for i, repl in enumerate(replace):
|
|
2670
|
+
off = offset[i] if i < len(offset) else offset[-1]
|
|
2671
|
+
if length and i < len(length):
|
|
2672
|
+
leng = length[i]
|
|
2673
|
+
result = result[:off] + repl + result[off + leng:]
|
|
2674
|
+
else:
|
|
2675
|
+
result = result[:off] + repl + result[off:]
|
|
2676
|
+
|
|
2677
|
+
return Stringable(result)
|
|
2678
|
+
|
|
2679
|
+
def scan(self, format_str: str) -> List[str]:
|
|
2680
|
+
"""
|
|
2681
|
+
Parse input from a string to a list, according to a format.
|
|
2682
|
+
|
|
2683
|
+
Parameters
|
|
2684
|
+
----------
|
|
2685
|
+
format_str : str
|
|
2686
|
+
Format string (simplified sscanf-like)
|
|
2687
|
+
|
|
2688
|
+
Returns
|
|
2689
|
+
-------
|
|
2690
|
+
list
|
|
2691
|
+
List of parsed values
|
|
2692
|
+
"""
|
|
2693
|
+
# Simplified implementation - convert format to regex
|
|
2694
|
+
# This is a basic implementation, not as full-featured as PHP's sscanf
|
|
2695
|
+
pattern = format_str.replace('%s', r'(\S+)').replace('%d', r'(\d+)').replace('%f', r'([\d.]+)')
|
|
2696
|
+
matches = re.findall(pattern, str(self))
|
|
2697
|
+
return list(matches[0]) if matches else []
|
|
2698
|
+
|
|
2699
|
+
def prepend(self, *values: str) -> "Stringable":
|
|
2700
|
+
"""
|
|
2701
|
+
Prepend the given values to the string.
|
|
2702
|
+
|
|
2703
|
+
Parameters
|
|
2704
|
+
----------
|
|
2705
|
+
values : str
|
|
2706
|
+
Values to prepend
|
|
2707
|
+
|
|
2708
|
+
Returns
|
|
2709
|
+
-------
|
|
2710
|
+
Stringable
|
|
2711
|
+
A new Stringable with prepended values
|
|
2712
|
+
"""
|
|
2713
|
+
return Stringable(''.join(values) + str(self))
|
|
2714
|
+
|
|
2715
|
+
def substr(self, start: int, length: Optional[int] = None) -> "Stringable":
|
|
2716
|
+
"""
|
|
2717
|
+
Returns the portion of the string specified by the start and length parameters.
|
|
2718
|
+
|
|
2719
|
+
Parameters
|
|
2720
|
+
----------
|
|
2721
|
+
start : int
|
|
2722
|
+
Starting position
|
|
2723
|
+
length : int, optional
|
|
2724
|
+
Length to extract, by default None
|
|
2725
|
+
|
|
2726
|
+
Returns
|
|
2727
|
+
-------
|
|
2728
|
+
Stringable
|
|
2729
|
+
A new Stringable with the substring
|
|
2730
|
+
"""
|
|
2731
|
+
s = str(self)
|
|
2732
|
+
if length is None:
|
|
2733
|
+
return Stringable(s[start:])
|
|
2734
|
+
else:
|
|
2735
|
+
return Stringable(s[start:start + length])
|
|
2736
|
+
|
|
2737
|
+
def doesntContain(self, needles: Union[str, List[str]], ignore_case: bool = False) -> bool:
|
|
2738
|
+
"""
|
|
2739
|
+
Determine if a given string doesn't contain a given substring.
|
|
2740
|
+
|
|
2741
|
+
Parameters
|
|
2742
|
+
----------
|
|
2743
|
+
needles : str or list
|
|
2744
|
+
The substring(s) to search for
|
|
2745
|
+
ignore_case : bool, optional
|
|
2746
|
+
Whether to ignore case, by default False
|
|
2747
|
+
|
|
2748
|
+
Returns
|
|
2749
|
+
-------
|
|
2750
|
+
bool
|
|
2751
|
+
True if string doesn't contain any needle, False otherwise
|
|
2752
|
+
"""
|
|
2753
|
+
return not self.contains(needles, ignore_case)
|
|
2754
|
+
|
|
2755
|
+
def doesntStartWith(self, needles: Union[str, List[str]]) -> bool:
|
|
2756
|
+
"""
|
|
2757
|
+
Determine if a given string doesn't start with a given substring.
|
|
2758
|
+
|
|
2759
|
+
Parameters
|
|
2760
|
+
----------
|
|
2761
|
+
needles : str or list
|
|
2762
|
+
The substring(s) to check
|
|
2763
|
+
|
|
2764
|
+
Returns
|
|
2765
|
+
-------
|
|
2766
|
+
bool
|
|
2767
|
+
True if string doesn't start with any needle, False otherwise
|
|
2768
|
+
"""
|
|
2769
|
+
if isinstance(needles, str):
|
|
2770
|
+
needles = [needles]
|
|
2771
|
+
return not any(str(self).startswith(needle) for needle in needles)
|
|
2772
|
+
|
|
2773
|
+
def doesntEndWith(self, needles: Union[str, List[str]]) -> bool:
|
|
2774
|
+
"""
|
|
2775
|
+
Determine if a given string doesn't end with a given substring.
|
|
2776
|
+
|
|
2777
|
+
Parameters
|
|
2778
|
+
----------
|
|
2779
|
+
needles : str or list
|
|
2780
|
+
The substring(s) to check
|
|
2781
|
+
|
|
2782
|
+
Returns
|
|
2783
|
+
-------
|
|
2784
|
+
bool
|
|
2785
|
+
True if string doesn't end with any needle, False otherwise
|
|
2786
|
+
"""
|
|
2787
|
+
return not self.endsWith(needles)
|
|
2788
|
+
|
|
2789
|
+
def startsWith(self, needles: Union[str, List[str]]) -> bool:
|
|
2790
|
+
"""
|
|
2791
|
+
Determine if a given string starts with a given substring.
|
|
2792
|
+
|
|
2793
|
+
Parameters
|
|
2794
|
+
----------
|
|
2795
|
+
needles : str or list
|
|
2796
|
+
The substring(s) to check
|
|
2797
|
+
|
|
2798
|
+
Returns
|
|
2799
|
+
-------
|
|
2800
|
+
bool
|
|
2801
|
+
True if string starts with any needle, False otherwise
|
|
2802
|
+
"""
|
|
2803
|
+
if isinstance(needles, str):
|
|
2804
|
+
needles = [needles]
|
|
2805
|
+
return any(str(self).startswith(needle) for needle in needles)
|
|
2806
|
+
|
|
2807
|
+
def jsonSerialize(self) -> str:
|
|
2808
|
+
"""
|
|
2809
|
+
Convert the object to a string when JSON encoded.
|
|
2810
|
+
|
|
2811
|
+
Returns
|
|
2812
|
+
-------
|
|
2813
|
+
str
|
|
2814
|
+
The string representation for JSON serialization
|
|
2815
|
+
"""
|
|
2816
|
+
return str(self)
|
|
2817
|
+
|
|
2818
|
+
def offsetExists(self, offset: int) -> bool:
|
|
2819
|
+
"""
|
|
2820
|
+
Determine if the given offset exists.
|
|
2821
|
+
|
|
2822
|
+
Parameters
|
|
2823
|
+
----------
|
|
2824
|
+
offset : int
|
|
2825
|
+
The offset to check
|
|
2826
|
+
|
|
2827
|
+
Returns
|
|
2828
|
+
-------
|
|
2829
|
+
bool
|
|
2830
|
+
True if offset exists, False otherwise
|
|
2831
|
+
"""
|
|
2832
|
+
try:
|
|
2833
|
+
str(self)[offset]
|
|
2834
|
+
return True
|
|
2835
|
+
except IndexError:
|
|
2836
|
+
return False
|
|
2837
|
+
|
|
2838
|
+
def offsetGet(self, offset: int) -> str:
|
|
2839
|
+
"""
|
|
2840
|
+
Get the value at the given offset.
|
|
2841
|
+
|
|
2842
|
+
Parameters
|
|
2843
|
+
----------
|
|
2844
|
+
offset : int
|
|
2845
|
+
The offset to get
|
|
2846
|
+
|
|
2847
|
+
Returns
|
|
2848
|
+
-------
|
|
2849
|
+
str
|
|
2850
|
+
The character at the offset
|
|
2851
|
+
"""
|
|
2852
|
+
return str(self)[offset]
|
|
2853
|
+
|
|
2854
|
+
def isPattern(self, pattern: Union[str, List[str]], ignore_case: bool = False) -> bool:
|
|
2855
|
+
"""
|
|
2856
|
+
Determine if a given string matches a given pattern.
|
|
2857
|
+
|
|
2858
|
+
This method checks if the string matches any of the given patterns,
|
|
2859
|
+
which can include wildcards (* and ?). The matching can be case-sensitive
|
|
2860
|
+
or case-insensitive based on the ignore_case parameter.
|
|
2861
|
+
|
|
2862
|
+
Parameters
|
|
2863
|
+
----------
|
|
2864
|
+
pattern : str or List[str]
|
|
2865
|
+
Pattern(s) to match (supports wildcards * and ?).
|
|
2866
|
+
ignore_case : bool, optional
|
|
2867
|
+
Whether to ignore case, by default False.
|
|
2868
|
+
|
|
2869
|
+
Returns
|
|
2870
|
+
-------
|
|
2871
|
+
bool
|
|
2872
|
+
True if string matches any of the patterns, False otherwise.
|
|
2873
|
+
"""
|
|
2874
|
+
import fnmatch
|
|
2875
|
+
|
|
2876
|
+
# Normalize pattern to list for consistent processing
|
|
2877
|
+
if isinstance(pattern, str):
|
|
2878
|
+
patterns = [pattern]
|
|
2879
|
+
else:
|
|
2880
|
+
patterns = pattern
|
|
2881
|
+
|
|
2882
|
+
# Get string representation
|
|
2883
|
+
s = str(self)
|
|
2884
|
+
|
|
2885
|
+
# Apply case-insensitive matching if requested
|
|
2886
|
+
if ignore_case:
|
|
2887
|
+
s = s.lower()
|
|
2888
|
+
patterns = [p.lower() for p in patterns]
|
|
2889
|
+
|
|
2890
|
+
# Check if string matches any of the patterns
|
|
2891
|
+
return any(fnmatch.fnmatch(s, p) for p in patterns)
|
|
2892
|
+
|
|
2893
|
+
def containsAll(self, needles: List[str], ignore_case: bool = False) -> bool:
|
|
2894
|
+
"""
|
|
2895
|
+
Determine if a given string contains all array values.
|
|
2896
|
+
|
|
2897
|
+
Parameters
|
|
2898
|
+
----------
|
|
2899
|
+
needles : list
|
|
2900
|
+
List of substrings to search for
|
|
2901
|
+
ignore_case : bool, optional
|
|
2902
|
+
Whether to ignore case, by default False
|
|
2903
|
+
|
|
2904
|
+
Returns
|
|
2905
|
+
-------
|
|
2906
|
+
bool
|
|
2907
|
+
True if string contains all needles, False otherwise
|
|
2908
|
+
"""
|
|
2909
|
+
s = str(self)
|
|
2910
|
+
if ignore_case:
|
|
2911
|
+
s = s.lower()
|
|
2912
|
+
needles = [needle.lower() for needle in needles]
|
|
2913
|
+
|
|
2914
|
+
return all(needle in s for needle in needles)
|
|
2915
|
+
|
|
2916
|
+
def whenIs(self, pattern: Union[str, List[str]], callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2917
|
+
"""
|
|
2918
|
+
Execute the given callback if the string matches a given pattern.
|
|
2919
|
+
|
|
2920
|
+
Parameters
|
|
2921
|
+
----------
|
|
2922
|
+
pattern : str or list
|
|
2923
|
+
Pattern(s) to match against
|
|
2924
|
+
callback : callable
|
|
2925
|
+
The callback to execute if condition is true
|
|
2926
|
+
default : callable, optional
|
|
2927
|
+
The callback to execute if condition is false, by default None
|
|
2928
|
+
|
|
2929
|
+
Returns
|
|
2930
|
+
-------
|
|
2931
|
+
Stringable
|
|
2932
|
+
Result of callback execution or self
|
|
2933
|
+
"""
|
|
2934
|
+
return self.when(self.isPattern(pattern), callback, default)
|
|
2935
|
+
|
|
2936
|
+
def whenIsAscii(self, callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2937
|
+
"""
|
|
2938
|
+
Execute the given callback if the string is 7 bit ASCII.
|
|
2939
|
+
|
|
2940
|
+
Parameters
|
|
2941
|
+
----------
|
|
2942
|
+
callback : callable
|
|
2943
|
+
The callback to execute if condition is true
|
|
2944
|
+
default : callable, optional
|
|
2945
|
+
The callback to execute if condition is false, by default None
|
|
2946
|
+
|
|
2947
|
+
Returns
|
|
2948
|
+
-------
|
|
2949
|
+
Stringable
|
|
2950
|
+
Result of callback execution or self
|
|
2951
|
+
"""
|
|
2952
|
+
return self.when(self.isAscii(), callback, default)
|
|
2953
|
+
|
|
2954
|
+
def whenIsUuid(self, callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2955
|
+
"""
|
|
2956
|
+
Execute the given callback if the string is a valid UUID.
|
|
2957
|
+
|
|
2958
|
+
Parameters
|
|
2959
|
+
----------
|
|
2960
|
+
callback : callable
|
|
2961
|
+
The callback to execute if condition is true
|
|
2962
|
+
default : callable, optional
|
|
2963
|
+
The callback to execute if condition is false, by default None
|
|
2964
|
+
|
|
2965
|
+
Returns
|
|
2966
|
+
-------
|
|
2967
|
+
Stringable
|
|
2968
|
+
Result of callback execution or self
|
|
2969
|
+
"""
|
|
2970
|
+
return self.when(self.isUuid(), callback, default)
|
|
2971
|
+
|
|
2972
|
+
def whenIsUlid(self, callback: Callable, default: Optional[Callable] = None) -> "Stringable":
|
|
2973
|
+
"""
|
|
2974
|
+
Execute the given callback if the string is a valid ULID.
|
|
2975
|
+
|
|
2976
|
+
Parameters
|
|
2977
|
+
----------
|
|
2978
|
+
callback : callable
|
|
2979
|
+
The callback to execute if condition is true
|
|
2980
|
+
default : callable, optional
|
|
2981
|
+
The callback to execute if condition is false, by default None
|
|
2982
|
+
|
|
2983
|
+
Returns
|
|
2984
|
+
-------
|
|
2985
|
+
Stringable
|
|
2986
|
+
Result of callback execution or self
|
|
2987
|
+
"""
|
|
2988
|
+
return self.when(self.isUlid(), callback, default)
|
|
2989
|
+
|
|
2990
|
+
|
|
2991
|
+
|
|
2992
|
+
def toDate(self, format_str: Optional[str] = None) -> Optional[datetime]:
|
|
2993
|
+
"""
|
|
2994
|
+
Convert the string to a datetime object.
|
|
2995
|
+
|
|
2996
|
+
Parameters
|
|
2997
|
+
----------
|
|
2998
|
+
format_str : str, optional
|
|
2999
|
+
Format string for parsing, by default None (auto-detect)
|
|
3000
|
+
|
|
3001
|
+
Returns
|
|
3002
|
+
-------
|
|
3003
|
+
datetime or None
|
|
3004
|
+
Parsed datetime object or None if parsing fails
|
|
3005
|
+
"""
|
|
3006
|
+
|
|
3007
|
+
s = str(self)
|
|
3008
|
+
|
|
3009
|
+
if format_str:
|
|
3010
|
+
try:
|
|
3011
|
+
return datetime.strptime(s, format_str)
|
|
3012
|
+
except ValueError:
|
|
3013
|
+
return None
|
|
3014
|
+
|
|
3015
|
+
# Try common date formats
|
|
3016
|
+
common_formats = [
|
|
3017
|
+
'%Y-%m-%d',
|
|
3018
|
+
'%Y-%m-%d %H:%M:%S',
|
|
3019
|
+
'%Y-%m-%dT%H:%M:%S',
|
|
3020
|
+
'%Y-%m-%d %H:%M',
|
|
3021
|
+
'%d/%m/%Y',
|
|
3022
|
+
'%m/%d/%Y',
|
|
3023
|
+
'%d-%m-%Y',
|
|
3024
|
+
'%m-%d-%Y'
|
|
3025
|
+
]
|
|
3026
|
+
|
|
3027
|
+
for fmt in common_formats:
|
|
3028
|
+
try:
|
|
3029
|
+
return datetime.strptime(s, fmt)
|
|
3030
|
+
except ValueError:
|
|
3031
|
+
continue
|
|
3032
|
+
|
|
3033
|
+
return None
|
|
3034
|
+
|
|
3035
|
+
def encrypt(self) -> "Stringable":
|
|
3036
|
+
"""
|
|
3037
|
+
Encrypt the string (placeholder implementation).
|
|
3038
|
+
|
|
3039
|
+
Note: This is a placeholder. In a real implementation, you would use
|
|
3040
|
+
a proper encryption library like cryptography.
|
|
3041
|
+
|
|
3042
|
+
Parameters
|
|
3043
|
+
----------
|
|
3044
|
+
None
|
|
3045
|
+
|
|
3046
|
+
Returns
|
|
3047
|
+
-------
|
|
3048
|
+
Stringable
|
|
3049
|
+
Encrypted string (base64 encoded for this placeholder)
|
|
3050
|
+
"""
|
|
3051
|
+
|
|
3052
|
+
return self.toBase64()
|
|
3053
|
+
|
|
3054
|
+
def decrypt(self) -> "Stringable":
|
|
3055
|
+
"""
|
|
3056
|
+
Decrypt the string (placeholder implementation).
|
|
3057
|
+
|
|
3058
|
+
Note: This is a placeholder. In a real implementation, you would use
|
|
3059
|
+
a proper decryption library like cryptography.
|
|
3060
|
+
|
|
3061
|
+
Parameters
|
|
3062
|
+
----------
|
|
3063
|
+
None
|
|
3064
|
+
|
|
3065
|
+
Returns
|
|
3066
|
+
-------
|
|
3067
|
+
Stringable
|
|
3068
|
+
Decrypted string
|
|
3069
|
+
"""
|
|
3070
|
+
|
|
3071
|
+
return self.fromBase64()
|
|
3072
|
+
|
|
3073
|
+
def toHtmlString(self) -> "Stringable":
|
|
3074
|
+
"""
|
|
3075
|
+
Create an HTML string representation (placeholder).
|
|
3076
|
+
|
|
3077
|
+
Returns
|
|
3078
|
+
-------
|
|
3079
|
+
Stringable
|
|
3080
|
+
HTML-safe string
|
|
3081
|
+
"""
|
|
3082
|
+
# Escape HTML entities
|
|
3083
|
+
return Stringable(html.escape(str(self)))
|
|
3084
|
+
|
|
3085
|
+
def tap(self, callback: Callable) -> "Stringable":
|
|
3086
|
+
"""
|
|
3087
|
+
Call the given callback with the string and return the string.
|
|
3088
|
+
|
|
3089
|
+
Parameters
|
|
3090
|
+
----------
|
|
3091
|
+
callback : callable
|
|
3092
|
+
The callback to execute with the string
|
|
3093
|
+
|
|
3094
|
+
Returns
|
|
3095
|
+
-------
|
|
3096
|
+
Stringable
|
|
3097
|
+
The same Stringable instance
|
|
3098
|
+
"""
|
|
3099
|
+
callback(self)
|
|
3100
|
+
return self
|