data.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import decimal
  2. from itertools import count, groupby
  3. __all__ = (
  4. 'array_to_ranges',
  5. 'array_to_string',
  6. 'deepmerge',
  7. 'drange',
  8. 'flatten_dict',
  9. 'shallow_compare_dict',
  10. )
  11. #
  12. # Dictionary utilities
  13. #
  14. def deepmerge(original, new):
  15. """
  16. Deep merge two dictionaries (new into original) and return a new dict
  17. """
  18. merged = dict(original)
  19. for key, val in new.items():
  20. if key in original and isinstance(original[key], dict) and val and isinstance(val, dict):
  21. merged[key] = deepmerge(original[key], val)
  22. else:
  23. merged[key] = val
  24. return merged
  25. def flatten_dict(d, prefix='', separator='.'):
  26. """
  27. Flatten nested dictionaries into a single level by joining key names with a separator.
  28. :param d: The dictionary to be flattened
  29. :param prefix: Initial prefix (if any)
  30. :param separator: The character to use when concatenating key names
  31. """
  32. ret = {}
  33. for k, v in d.items():
  34. key = separator.join([prefix, k]) if prefix else k
  35. if type(v) is dict:
  36. ret.update(flatten_dict(v, prefix=key, separator=separator))
  37. else:
  38. ret[key] = v
  39. return ret
  40. def shallow_compare_dict(source_dict, destination_dict, exclude=tuple()):
  41. """
  42. Return a new dictionary of the different keys. The values of `destination_dict` are returned. Only the equality of
  43. the first layer of keys/values is checked. `exclude` is a list or tuple of keys to be ignored.
  44. """
  45. difference = {}
  46. for key, value in destination_dict.items():
  47. if key in exclude:
  48. continue
  49. if source_dict.get(key) != value:
  50. difference[key] = value
  51. return difference
  52. #
  53. # Array utilities
  54. #
  55. def array_to_ranges(array):
  56. """
  57. Convert an arbitrary array of integers to a list of consecutive values. Nonconsecutive values are returned as
  58. single-item tuples. For example:
  59. [0, 1, 2, 10, 14, 15, 16] => [(0, 2), (10,), (14, 16)]"
  60. """
  61. group = (
  62. list(x) for _, x in groupby(sorted(array), lambda x, c=count(): next(c) - x)
  63. )
  64. return [
  65. (g[0], g[-1])[:len(g)] for g in group
  66. ]
  67. def array_to_string(array):
  68. """
  69. Generate an efficient, human-friendly string from a set of integers. Intended for use with ArrayField.
  70. For example:
  71. [0, 1, 2, 10, 14, 15, 16] => "0-2, 10, 14-16"
  72. """
  73. ret = []
  74. ranges = array_to_ranges(array)
  75. for value in ranges:
  76. if len(value) == 1:
  77. ret.append(str(value[0]))
  78. else:
  79. ret.append(f'{value[0]}-{value[1]}')
  80. return ', '.join(ret)
  81. #
  82. # Range utilities
  83. #
  84. def drange(start, end, step=decimal.Decimal(1)):
  85. """
  86. Decimal-compatible implementation of Python's range()
  87. """
  88. start, end, step = decimal.Decimal(start), decimal.Decimal(end), decimal.Decimal(step)
  89. if start < end:
  90. while start < end:
  91. yield start
  92. start += step
  93. else:
  94. while start > end:
  95. yield start
  96. start += step