string.py 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. import re
  2. __all__ = (
  3. 'enum_key',
  4. 'humanize_duration',
  5. 'remove_linebreaks',
  6. 'title',
  7. 'trailing_slash',
  8. )
  9. def humanize_duration(value):
  10. """
  11. Express a timedelta in a human-friendly format. Example: 1h 5m 23s. Durations of a second or
  12. more are rounded to whole seconds; shorter durations are rounded to the millisecond (e.g.
  13. 0.43s). A negative duration is rendered with a leading minus sign, so that an anomalous value
  14. remains recognizable as one. Returns an empty string for None; zero renders as "0s".
  15. """
  16. if value is None:
  17. return ''
  18. total_seconds = value.total_seconds()
  19. magnitude = abs(total_seconds)
  20. # Render sub-second durations to the millisecond, as rounding them to whole seconds would
  21. # report every short-lived duration as zero. Trailing zeros are stripped.
  22. if 0 < magnitude < 1:
  23. rendered = f'{magnitude:.3f}'.rstrip('0').rstrip('.')
  24. # A magnitude below a millisecond has no representation here, so fall through to "0s".
  25. # Rounding up to a whole second (e.g. 0.9996) likewise falls through, to "1s".
  26. if rendered not in ('0', '1'):
  27. return f'-{rendered}s' if total_seconds < 0 else f'{rendered}s'
  28. # Round to whole seconds and decompose
  29. days, remainder = divmod(round(magnitude), 86400)
  30. hours, remainder = divmod(remainder, 3600)
  31. minutes, seconds = divmod(remainder, 60)
  32. ret = ''
  33. if days:
  34. ret += f'{days}d '
  35. if hours:
  36. ret += f'{hours}h '
  37. if minutes:
  38. ret += f'{minutes}m '
  39. if seconds or not ret:
  40. ret += f'{seconds}s'
  41. ret = ret.strip()
  42. # Zero carries no sign, however the original value was signed
  43. if total_seconds < 0 and ret != '0s':
  44. ret = f'-{ret}'
  45. return ret
  46. def enum_key(value):
  47. """
  48. Convert the given value to a string suitable for use as an Enum key.
  49. """
  50. value = str(value).upper()
  51. return re.sub(r'[^_A-Z0-9]', '_', value)
  52. def remove_linebreaks(value):
  53. """
  54. Remove all line breaks from a string and return the result. Useful for log sanitization purposes.
  55. """
  56. return value.replace('\n', '').replace('\r', '')
  57. def title(value):
  58. """
  59. Improved implementation of str.title(); retains all existing uppercase letters.
  60. """
  61. return ' '.join([w[0].upper() + w[1:] for w in str(value).split()])
  62. def trailing_slash(value):
  63. """
  64. Remove a leading slash (if any) and include a trailing slash, except for empty strings.
  65. """
  66. return f'{value.strip("/")}/' if value else ''