Module: case_util
Expand source code
# Copyright (C) 2023-present The Project Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from typing import Pattern
from cl.runtime.primitive.char_util import CharUtil
from cl.runtime.primitive.string_util import StringUtil
_alphanumeric_re: Pattern = re.compile(r"[^a-zA-Z0-9 .]")
"""Match sequences where all characters are either letters or digits, also allowing dot and space."""
_alphanumeric_or_underscore_re: Pattern = re.compile(r"[^a-zA-Z0-9 ._]")
"""Match sequences where all characters are either letters or digits or an underscore, also allowing dot and space."""
_all_cap_re: Pattern = re.compile(r"([a-z])([A-Z])")
"""Match sequences where a lowercase letter ([a-z]) is immediately followed by an uppercase letter ([A-Z])"""
_digit_separator_re: Pattern = re.compile(r"([a-zA-Z])(d)")
"""Pattern to add underscores before digits (e.g., "Abc2" -> "abc_2")"""
_consecutive_cap_re: Pattern = re.compile(r"([A-Z])([A-Z])")
"""This pattern looks for uppercase sequences and adds an underscore between them if needed"""
_digit_without_underscore_re: Pattern = re.compile(r"(?<!_)d")
"""Digit without underscore pattern"""
_digit_without_space_re: Pattern = re.compile(r"(?<! )d")
"""Digit without space pattern"""
class CaseUtil:
"""Utilities for case conversion and other operations on string."""
@classmethod
def pascal_to_snake_case(cls, value: str | None) -> str | None:
"""Convert PascalCase to snake_case using custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
return value
cls.check_pascal_case(value)
# Add underscores between consecutive uppercase letters
result = _consecutive_cap_re.sub(r"1_2", value)
# Handle lowercase to uppercase transitions
result = _all_cap_re.sub(r"1_2", result)
# Insert underscore before digits
result = _digit_separator_re.sub(r"1_2", result)
# Convert the final result to lowercase
return result.lower()
@classmethod
def upper_to_snake_case(cls, value: str | None) -> str | None:
"""Convert UPPER_CASE to snake_case using custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
return value
cls.check_upper_case(value)
return value.lower()
@classmethod
def snake_to_upper_case(cls, value: str | None) -> str | None:
"""Convert snake_case to UPPER_CASE using custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
return value
cls.check_snake_case(value)
return value.upper()
@classmethod
def snake_to_pascal_case(cls, value: str | None) -> str | None:
"""Convert snake_case to PascalCase using custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
return value
cls.check_snake_case(value)
input_tokens = value.split(".")
# Apply the processing (i.e. `__pascalize_segment()`) function to each segment and join
# them into PascalCase.
# Finally, join the tokens with dots and return the result
return ".".join(
["".join(cls.__pascalize_segment(segment) for segment in token.split("_")) for token in input_tokens]
)
@classmethod
def upper_to_pascal_case(cls, value: str | None) -> str | None:
"""Convert UPPER_CASE to PascalCase using custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
return value
cls.check_upper_case(value)
return cls.snake_to_pascal_case(value.lower())
@classmethod
def pascal_to_upper_case(cls, value: str | None) -> str | None:
"""Convert PascalCase to UPPER_CASE using custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
return value
cls.check_pascal_case(value)
snake_case_value = cls.pascal_to_snake_case(value)
return snake_case_value.upper()
@classmethod
def pascal_to_title_case(cls, value: str | None) -> str | None:
"""Convert PascalCase to Title Case using custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
return value
cls.check_pascal_case(value)
snake_case_value = cls.pascal_to_snake_case(value)
# Apply the processing (i.e. `__pascalize_segment()`) function to each segment and join
# them into Title Case.
return " ".join(cls.__pascalize_segment(segment) for segment in snake_case_value.split("_"))
@classmethod
def snake_to_title_case(cls, value: str | None) -> str | None:
"""Convert snake_case to Title Case using custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
return value
cls.check_snake_case(value)
pascal_case_value = cls.snake_to_pascal_case(value)
return cls.pascal_to_title_case(pascal_case_value)
@classmethod
def snake_to_pascal_case_keep_trailing_underscore(cls, value: str | None):
"""
Convert snake_case_ to PascalCase_ using custom rule for separators in front of digits
and keep ending underscore.
"""
if StringUtil.is_empty(value):
return value
return cls.snake_to_pascal_case(value.removesuffix("_")) + ("_" if value.endswith("_") else "")
@classmethod
def pascale_to_snake_case_keep_trailing_underscore(cls, value: str | None):
"""
Convert PascalCase_ to snake_case_ using custom rule for separators in front of digits
and keep ending underscore.
"""
if StringUtil.is_empty(value):
return value
return cls.pascal_to_snake_case(value.removesuffix("_")) + ("_" if value.endswith("_") else "")
@classmethod
def check_snake_case(cls, value: str | None) -> None:
"""Error message if arg is not snake_case or does not follow custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
# Consider None or empty string compliant with the format
return
cls._check_non_alphanumeric(value, "snake_case", allow_underscore=True)
cls._check_no_space(value, "snake_case")
cls._check_no_upper(value, "snake_case")
cls._check_double_underscore(value, "snake_case")
cls._check_snake_case_digit_separator(value)
@classmethod
def check_pascal_case(cls, value: str | None) -> None:
"""Error message if arg is not PascalCase or does not follow custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
# Consider None or empty string compliant with the format
return
cls._check_non_alphanumeric(value, "PascalCase", allow_underscore=False)
cls._check_no_space(value, "PascalCase")
cls._check_no_underscore(value, "PascalCase")
cls._check_first_letter_capitalized(value, "PascalCase")
# NOTE: PascalCase shouldn't be checked for custom rule for separators in
# front of digits, because there's no separators
@classmethod
def check_title_case(cls, value: str | None) -> None:
"""Error message if arg is not Title Case or does not follow custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
# Consider None or empty string compliant with the format
return
cls._check_non_alphanumeric(value, "Title Case", allow_underscore=False)
cls._check_no_underscore(value, "Title Case")
cls._check_first_letter_capitalized(value, "Title Case")
cls._check_title_case_digit_separator(value)
@classmethod
def check_upper_case(cls, value: str | None) -> None:
"""Error message if arg is not UPPER_CASE or does not follow custom rule for separators in front of digits."""
if StringUtil.is_empty(value):
# Consider None or empty string compliant with the format
return
cls._check_non_alphanumeric(value, "UPPER_CASE", allow_underscore=True)
cls._check_no_space(value, "UPPER_CASE")
cls._check_no_lower(value, "UPPER_CASE")
cls._check_upper_case_digit_separator(value)
@classmethod
def is_pascal_case(cls, value: str) -> bool:
"""Check if the string is in PascalCase."""
try:
cls.check_pascal_case(value)
return True
except RuntimeError:
return False
@classmethod
def is_snake_case(cls, value: str) -> bool:
"""Check if the string is in snake_case."""
try:
cls.check_snake_case(value)
return True
except RuntimeError:
return False
@classmethod
def is_title_case(cls, value: str) -> bool:
"""Check if the string is in Title Case."""
try:
cls.check_title_case(value)
return True
except RuntimeError:
return False
@classmethod
def is_upper_case(cls, value: str) -> bool:
"""Check if the string is in UPPER_CASE."""
try:
cls.check_upper_case(value)
return True
except RuntimeError:
return False
@classmethod
def _check_non_alphanumeric(cls, value: str, format_: str, allow_underscore: bool) -> None:
"""Error message stating the string does not follow format because it contains non-alphanumeric characters."""
if allow_underscore:
non_alphanumeric = re.findall(_alphanumeric_or_underscore_re, value)
else:
non_alphanumeric = list(set(re.findall(_alphanumeric_re, value)))
if non_alphanumeric:
non_alphanumeric_names = ", ".join(CharUtil.describe_char(char) for char in non_alphanumeric)
other_than_underscore_msg = " other than underscore" if allow_underscore else ""
raise RuntimeError(
f"String '{value}' is not '{format_}' because it contains "
f"non-alphanumeric characters{other_than_underscore_msg}: "
f"{non_alphanumeric_names}"
)
@classmethod
def _check_no_space(cls, value: str, format_: str) -> None:
"""Error message stating string does not follow format if it contains a space."""
if " " in value:
raise RuntimeError(f"String {value} is not {format_} because it contains a space.")
@classmethod
def _check_no_underscore(cls, value: str, format_: str) -> None:
"""Error message stating string does not follow format if it contains an underscore."""
if "_" in value:
raise RuntimeError(f"String {value} is not {format_} because it contains an underscore.")
@classmethod
def _check_double_underscore(cls, value: str, format_: str) -> None:
"""Error message stating string does not follow format if it contains a double underscore."""
if "__" in value:
raise RuntimeError(f"String {value} is not {format_} because it contains a doubled underscore.")
@classmethod
def _check_no_lower(cls, value: str, format_: str) -> None:
"""Error message stating string does not follow format if it contains a lowercase character."""
if any(char.islower() for char in value):
raise RuntimeError(f"String {value} is not {format_} because it contains a lowercase character.")
@classmethod
def _check_no_upper(cls, value: str, format_: str) -> None:
"""Error message stating string does not follow format if it contains an uppercase character."""
if any(char.isupper() for char in value):
raise RuntimeError(f"String {value} is not {format_} because it contains an uppercase character.")
@classmethod
def _check_first_letter_capitalized(cls, value: str, format_: str) -> None:
"""Error message stating string does not follow format if it does not start with an uppercase letter."""
if not value[0].isupper():
raise RuntimeError(f"String {value} is not {format_} because the first letter is lowercase.")
@classmethod
def _check_snake_case_digit_separator(cls, value: str) -> None:
"""Error message stating string does not follow custom rule for separators in front of digits"""
# snake_case must have an underscore in front of digits
if _digit_without_underscore_re.search(value):
raise RuntimeError(
f"String {value} is not snake_case because it does not follow custom rule "
f"for separators in front of digits.",
)
@classmethod
def _check_title_case_digit_separator(cls, value: str) -> None:
"""Error message stating string does not follow custom rule for separators in front of digits"""
# Title Case must have a space in front of digits
if _digit_without_space_re.search(value):
raise RuntimeError(
f"String {value} is not Title Case because it does not follow custom rule "
f"for separators in front of digits.",
)
@classmethod
def _check_upper_case_digit_separator(cls, value: str) -> None:
"""Error message stating string does not follow custom rule for separators in front of digits"""
# Make a round trip from snake_case to PascalCase and back to snake_case to check
# if the value stays the same
if _digit_without_underscore_re.search(value):
raise RuntimeError(
f"String {value} is not UPPER_CASE because it does not follow custom rule "
f"for separators in front of digits.",
)
@classmethod
def __pascalize_segment(cls, segment: str) -> str:
"""
Pascalize a segment (substring between 2 underscores) from snake_case
using custom rule for separators in front of digits.
"""
# If the segment starts with a digit, capitalize only the first character after the digit
if segment and segment[0].isdigit():
return segment[0] + segment[1:].capitalize()
# Otherwise, capitalize the first letter of the segment
return segment.capitalize()
Classes
class CaseUtil
-
Utilities for case conversion and other operations on string.
Expand source code
class CaseUtil: """Utilities for case conversion and other operations on string.""" @classmethod def pascal_to_snake_case(cls, value: str | None) -> str | None: """Convert PascalCase to snake_case using custom rule for separators in front of digits.""" if StringUtil.is_empty(value): return value cls.check_pascal_case(value) # Add underscores between consecutive uppercase letters result = _consecutive_cap_re.sub(r"1_2", value) # Handle lowercase to uppercase transitions result = _all_cap_re.sub(r"1_2", result) # Insert underscore before digits result = _digit_separator_re.sub(r"1_2", result) # Convert the final result to lowercase return result.lower() @classmethod def upper_to_snake_case(cls, value: str | None) -> str | None: """Convert UPPER_CASE to snake_case using custom rule for separators in front of digits.""" if StringUtil.is_empty(value): return value cls.check_upper_case(value) return value.lower() @classmethod def snake_to_upper_case(cls, value: str | None) -> str | None: """Convert snake_case to UPPER_CASE using custom rule for separators in front of digits.""" if StringUtil.is_empty(value): return value cls.check_snake_case(value) return value.upper() @classmethod def snake_to_pascal_case(cls, value: str | None) -> str | None: """Convert snake_case to PascalCase using custom rule for separators in front of digits.""" if StringUtil.is_empty(value): return value cls.check_snake_case(value) input_tokens = value.split(".") # Apply the processing (i.e. `__pascalize_segment()`) function to each segment and join # them into PascalCase. # Finally, join the tokens with dots and return the result return ".".join( ["".join(cls.__pascalize_segment(segment) for segment in token.split("_")) for token in input_tokens] ) @classmethod def upper_to_pascal_case(cls, value: str | None) -> str | None: """Convert UPPER_CASE to PascalCase using custom rule for separators in front of digits.""" if StringUtil.is_empty(value): return value cls.check_upper_case(value) return cls.snake_to_pascal_case(value.lower()) @classmethod def pascal_to_upper_case(cls, value: str | None) -> str | None: """Convert PascalCase to UPPER_CASE using custom rule for separators in front of digits.""" if StringUtil.is_empty(value): return value cls.check_pascal_case(value) snake_case_value = cls.pascal_to_snake_case(value) return snake_case_value.upper() @classmethod def pascal_to_title_case(cls, value: str | None) -> str | None: """Convert PascalCase to Title Case using custom rule for separators in front of digits.""" if StringUtil.is_empty(value): return value cls.check_pascal_case(value) snake_case_value = cls.pascal_to_snake_case(value) # Apply the processing (i.e. `__pascalize_segment()`) function to each segment and join # them into Title Case. return " ".join(cls.__pascalize_segment(segment) for segment in snake_case_value.split("_")) @classmethod def snake_to_title_case(cls, value: str | None) -> str | None: """Convert snake_case to Title Case using custom rule for separators in front of digits.""" if StringUtil.is_empty(value): return value cls.check_snake_case(value) pascal_case_value = cls.snake_to_pascal_case(value) return cls.pascal_to_title_case(pascal_case_value) @classmethod def snake_to_pascal_case_keep_trailing_underscore(cls, value: str | None): """ Convert snake_case_ to PascalCase_ using custom rule for separators in front of digits and keep ending underscore. """ if StringUtil.is_empty(value): return value return cls.snake_to_pascal_case(value.removesuffix("_")) + ("_" if value.endswith("_") else "") @classmethod def pascale_to_snake_case_keep_trailing_underscore(cls, value: str | None): """ Convert PascalCase_ to snake_case_ using custom rule for separators in front of digits and keep ending underscore. """ if StringUtil.is_empty(value): return value return cls.pascal_to_snake_case(value.removesuffix("_")) + ("_" if value.endswith("_") else "") @classmethod def check_snake_case(cls, value: str | None) -> None: """Error message if arg is not snake_case or does not follow custom rule for separators in front of digits.""" if StringUtil.is_empty(value): # Consider None or empty string compliant with the format return cls._check_non_alphanumeric(value, "snake_case", allow_underscore=True) cls._check_no_space(value, "snake_case") cls._check_no_upper(value, "snake_case") cls._check_double_underscore(value, "snake_case") cls._check_snake_case_digit_separator(value) @classmethod def check_pascal_case(cls, value: str | None) -> None: """Error message if arg is not PascalCase or does not follow custom rule for separators in front of digits.""" if StringUtil.is_empty(value): # Consider None or empty string compliant with the format return cls._check_non_alphanumeric(value, "PascalCase", allow_underscore=False) cls._check_no_space(value, "PascalCase") cls._check_no_underscore(value, "PascalCase") cls._check_first_letter_capitalized(value, "PascalCase") # NOTE: PascalCase shouldn't be checked for custom rule for separators in # front of digits, because there's no separators @classmethod def check_title_case(cls, value: str | None) -> None: """Error message if arg is not Title Case or does not follow custom rule for separators in front of digits.""" if StringUtil.is_empty(value): # Consider None or empty string compliant with the format return cls._check_non_alphanumeric(value, "Title Case", allow_underscore=False) cls._check_no_underscore(value, "Title Case") cls._check_first_letter_capitalized(value, "Title Case") cls._check_title_case_digit_separator(value) @classmethod def check_upper_case(cls, value: str | None) -> None: """Error message if arg is not UPPER_CASE or does not follow custom rule for separators in front of digits.""" if StringUtil.is_empty(value): # Consider None or empty string compliant with the format return cls._check_non_alphanumeric(value, "UPPER_CASE", allow_underscore=True) cls._check_no_space(value, "UPPER_CASE") cls._check_no_lower(value, "UPPER_CASE") cls._check_upper_case_digit_separator(value) @classmethod def is_pascal_case(cls, value: str) -> bool: """Check if the string is in PascalCase.""" try: cls.check_pascal_case(value) return True except RuntimeError: return False @classmethod def is_snake_case(cls, value: str) -> bool: """Check if the string is in snake_case.""" try: cls.check_snake_case(value) return True except RuntimeError: return False @classmethod def is_title_case(cls, value: str) -> bool: """Check if the string is in Title Case.""" try: cls.check_title_case(value) return True except RuntimeError: return False @classmethod def is_upper_case(cls, value: str) -> bool: """Check if the string is in UPPER_CASE.""" try: cls.check_upper_case(value) return True except RuntimeError: return False @classmethod def _check_non_alphanumeric(cls, value: str, format_: str, allow_underscore: bool) -> None: """Error message stating the string does not follow format because it contains non-alphanumeric characters.""" if allow_underscore: non_alphanumeric = re.findall(_alphanumeric_or_underscore_re, value) else: non_alphanumeric = list(set(re.findall(_alphanumeric_re, value))) if non_alphanumeric: non_alphanumeric_names = ", ".join(CharUtil.describe_char(char) for char in non_alphanumeric) other_than_underscore_msg = " other than underscore" if allow_underscore else "" raise RuntimeError( f"String '{value}' is not '{format_}' because it contains " f"non-alphanumeric characters{other_than_underscore_msg}: " f"{non_alphanumeric_names}" ) @classmethod def _check_no_space(cls, value: str, format_: str) -> None: """Error message stating string does not follow format if it contains a space.""" if " " in value: raise RuntimeError(f"String {value} is not {format_} because it contains a space.") @classmethod def _check_no_underscore(cls, value: str, format_: str) -> None: """Error message stating string does not follow format if it contains an underscore.""" if "_" in value: raise RuntimeError(f"String {value} is not {format_} because it contains an underscore.") @classmethod def _check_double_underscore(cls, value: str, format_: str) -> None: """Error message stating string does not follow format if it contains a double underscore.""" if "__" in value: raise RuntimeError(f"String {value} is not {format_} because it contains a doubled underscore.") @classmethod def _check_no_lower(cls, value: str, format_: str) -> None: """Error message stating string does not follow format if it contains a lowercase character.""" if any(char.islower() for char in value): raise RuntimeError(f"String {value} is not {format_} because it contains a lowercase character.") @classmethod def _check_no_upper(cls, value: str, format_: str) -> None: """Error message stating string does not follow format if it contains an uppercase character.""" if any(char.isupper() for char in value): raise RuntimeError(f"String {value} is not {format_} because it contains an uppercase character.") @classmethod def _check_first_letter_capitalized(cls, value: str, format_: str) -> None: """Error message stating string does not follow format if it does not start with an uppercase letter.""" if not value[0].isupper(): raise RuntimeError(f"String {value} is not {format_} because the first letter is lowercase.") @classmethod def _check_snake_case_digit_separator(cls, value: str) -> None: """Error message stating string does not follow custom rule for separators in front of digits""" # snake_case must have an underscore in front of digits if _digit_without_underscore_re.search(value): raise RuntimeError( f"String {value} is not snake_case because it does not follow custom rule " f"for separators in front of digits.", ) @classmethod def _check_title_case_digit_separator(cls, value: str) -> None: """Error message stating string does not follow custom rule for separators in front of digits""" # Title Case must have a space in front of digits if _digit_without_space_re.search(value): raise RuntimeError( f"String {value} is not Title Case because it does not follow custom rule " f"for separators in front of digits.", ) @classmethod def _check_upper_case_digit_separator(cls, value: str) -> None: """Error message stating string does not follow custom rule for separators in front of digits""" # Make a round trip from snake_case to PascalCase and back to snake_case to check # if the value stays the same if _digit_without_underscore_re.search(value): raise RuntimeError( f"String {value} is not UPPER_CASE because it does not follow custom rule " f"for separators in front of digits.", ) @classmethod def __pascalize_segment(cls, segment: str) -> str: """ Pascalize a segment (substring between 2 underscores) from snake_case using custom rule for separators in front of digits. """ # If the segment starts with a digit, capitalize only the first character after the digit if segment and segment[0].isdigit(): return segment[0] + segment[1:].capitalize() # Otherwise, capitalize the first letter of the segment return segment.capitalize()
Static methods
def check_pascal_case(value: str | None) -> None
-
Error message if arg is not PascalCase or does not follow custom rule for separators in front of digits.
def check_snake_case(value: str | None) -> None
-
Error message if arg is not snake_case or does not follow custom rule for separators in front of digits.
def check_title_case(value: str | None) -> None
-
Error message if arg is not Title Case or does not follow custom rule for separators in front of digits.
def check_upper_case(value: str | None) -> None
-
Error message if arg is not UPPER_CASE or does not follow custom rule for separators in front of digits.
def is_pascal_case(value: str) -> bool
-
Check if the string is in PascalCase.
def is_snake_case(value: str) -> bool
-
Check if the string is in snake_case.
def is_title_case(value: str) -> bool
-
Check if the string is in Title Case.
def is_upper_case(value: str) -> bool
-
Check if the string is in UPPER_CASE.
def pascal_to_snake_case(value: str | None) -> str | None
-
Convert PascalCase to snake_case using custom rule for separators in front of digits.
def pascal_to_title_case(value: str | None) -> str | None
-
Convert PascalCase to Title Case using custom rule for separators in front of digits.
def pascal_to_upper_case(value: str | None) -> str | None
-
Convert PascalCase to UPPER_CASE using custom rule for separators in front of digits.
def pascale_to_snake_case_keep_trailing_underscore(value: str | None)
-
Convert PascalCase_ to snake_case_ using custom rule for separators in front of digits and keep ending underscore.
def snake_to_pascal_case(value: str | None) -> str | None
-
Convert snake_case to PascalCase using custom rule for separators in front of digits.
def snake_to_pascal_case_keep_trailing_underscore(value: str | None)
-
Convert snake_case_ to PascalCase_ using custom rule for separators in front of digits and keep ending underscore.
def snake_to_title_case(value: str | None) -> str | None
-
Convert snake_case to Title Case using custom rule for separators in front of digits.
def snake_to_upper_case(value: str | None) -> str | None
-
Convert snake_case to UPPER_CASE using custom rule for separators in front of digits.
def upper_to_pascal_case(value: str | None) -> str | None
-
Convert UPPER_CASE to PascalCase using custom rule for separators in front of digits.
def upper_to_snake_case(value: str | None) -> str | None
-
Convert UPPER_CASE to snake_case using custom rule for separators in front of digits.