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.
@@ -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