Skip to content

Commit

Permalink
Merge 52e51a0 into f87365a
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexandreDecan committed Aug 6, 2020
2 parents f87365a + 52e51a0 commit 66f0264
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 96 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog


## 2.1.0 (not yet released)

### Added
- `IntervalDict.as_dict()` to export its content to a classical Python `dict`.

## Changed
- `IntervalDict.keys()`, `values()` and `items()` return view objects instead of lists.

## Fixed
- `IntervalDict.popitem()` now returns a (key, value) pair instead of an `IntervalDict`.
- The documentation of `IntervalDict.pop()` now correctly states that the value (and not the key)
is returned.



## 2.0.2 (2020-05-09)

### Fixed
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The `portion` library (formerly distributed as `python-intervals`) provides data
- Heavily tested with high code coverage.

**Latest release:**
- `portion`: 2.0.2 on 2020-05-09 ([documentation](https://github.com/AlexandreDecan/portion/blob/2.0.2/README.md), [changes](https://github.com/AlexandreDecan/portion/blob/2.0.2/CHANGELOG.md)).
- `portion`: 2.1.0 on 2020-05-09 ([documentation](https://github.com/AlexandreDecan/portion/blob/2.1.0/README.md), [changes](https://github.com/AlexandreDecan/portion/blob/2.1.0/CHANGELOG.md)).
- `python-intervals`: 1.10.0 on 2019-09-26 ([documentation](https://github.com/AlexandreDecan/portion/blob/1.10.0/README.md), [changes](https://github.com/AlexandreDecan/portion/blob/1.10.0/README.md#changelog)).

Note that `python-intervals` will no longer receive updates since it has been replaced by `portion`.
Expand Down Expand Up @@ -651,17 +651,17 @@ value is defined:
```

The active domain of an `IntervalDict` can be retrieved with its `.domain` method.
This method always returns a single `Interval` instance, where `.keys` returns a list
of disjoint intervals, one for each stored value.
This method always returns a single `Interval` instance, where `.keys` returns
disjoint intervals, one for each stored value.

```python
>>> d.domain()
[0,4]
>>> d.keys()
>>> list(d.keys())
[[0,2), [2,4]]
>>> d.values()
>>> list(d.values())
['banana', 'orange']
>>> d.items()
>>> list(d.items())
[([0,2), 'banana'), ([2,4], 'orange')]

```
Expand Down Expand Up @@ -697,6 +697,8 @@ by querying the resulting `IntervalDict` as follows:

Finally, similarly to a `dict`, an `IntervalDict` also supports `len`, `in` and `del`, and defines
`.clear`, `.copy`, `.update`, `.pop`, `.popitem`, and `.setdefault`.
For convenience, one can export the content of an `IntervalDict` to a classical Python `dict` using
the `as_dict` method.


[↑ back to top](#table-of-contents)
Expand Down
112 changes: 58 additions & 54 deletions portion/dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from collections.abc import MutableMapping, Mapping

from sortedcontainers import SortedDict


def _sort(i):
return (i[0].lower, i[0].left is Bound.CLOSED, i[0].upper, i[0].right is Bound.OPEN)
Expand All @@ -25,7 +27,7 @@ class IntervalDict(MutableMapping):
number of distinct values (not keys) that are stored.
"""

__slots__ = ('_items', )
__slots__ = ('_storage', )

def __init__(self, mapping_or_iterable=None):
"""
Expand All @@ -38,7 +40,7 @@ def __init__(self, mapping_or_iterable=None):
:param mapping_or_iterable: optional mapping or iterable.
"""
self._items = list() # List of (interval, value) pairs
self._storage = SortedDict(_sort) # Mapping from intervals to values

if mapping_or_iterable is not None:
self.update(mapping_or_iterable)
Expand All @@ -47,7 +49,7 @@ def clear(self):
"""
Remove all items from the IntervalDict.
"""
self._items.clear()
self._storage.clear()

def copy(self):
"""
Expand Down Expand Up @@ -88,43 +90,44 @@ def find(self, value):
:param value: value to look for.
:return: an Interval instance.
"""
return Interval(*(i for i, v in self._items if v == value))
return Interval(*(i for i, v in self._storage.items() if v == value))

def items(self):
"""
Return a sorted list of (Interval, value) pairs.
Return a set-like object providing a view on contained items.
:return: a sorted list of 2-uples.
:return: a set-like object.
"""
return sorted(self._items, key=_sort)
return self._storage.items()

def keys(self):
"""
Return the list of underlying Interval instances.
Return a set-like object providing a view on existing keys.
:return: a list of intervals.
:return: a set-like object.
"""
return [i for i, v in self.items()]
return self._storage.keys()

def values(self):
"""
Return the list of values.
Return a set-like object providing a view on contained values.
:return: a list of values.
:return: a set-like object.
"""
return [v for i, v in self.items()]
return self._storage.values()

def domain(self):
"""
Return an Interval corresponding to the domain of this IntervalDict.
:return: an Interval.
"""
return Interval(*(i for i, v in self._items))
return Interval(*self._storage.keys())

def pop(self, key, default=None):
"""
Return and remove given key.
Remove key and return the corresponding value if key is not an Interval.
If key is an interval, it returns an IntervalDict instance.
This method combines self[key] and del self[key]. If a default value
is provided and is not None, it uses self.get(key, default) instead of
Expand All @@ -148,14 +151,12 @@ def pop(self, key, default=None):

def popitem(self):
"""
Pop an arbitrary existing key.
Remove and return some (key, value) pair as a 2-tuple.
Raise KeyError if D is empty.
:return: an IntervalDict
:return: a (key, value) pair.
"""
try:
return self.pop(self._items[-1][0])
except IndexError:
raise KeyError('Instance is empty.')
return self._storage.popitem()

def setdefault(self, key, default=None):
"""
Expand Down Expand Up @@ -196,20 +197,6 @@ def update(self, mapping_or_iterable):
i = singleton(i) if not isinstance(i, Interval) else i
self[i] = v

def __getitem__(self, key):
if isinstance(key, Interval):
items = []
for i, v in self._items:
intersection = key & i
if not intersection.empty:
items.append((intersection, v))
return IntervalDict(items)
else:
for i, v in self._items:
if key in i:
return v
raise KeyError(key)

def combine(self, other, how):
"""
Return a new IntervalDict that combines the values from current and
Expand Down Expand Up @@ -243,57 +230,74 @@ def combine(self, other, how):

return IntervalDict(new_items)

def as_dict(self):
"""
Return the content as a classical Python dict.
:return: a Python dict.
"""
return dict(self._storage)

def __getitem__(self, key):
if isinstance(key, Interval):
items = []
for i, v in self._storage.items():
intersection = key & i
if not intersection.empty:
items.append((intersection, v))
return IntervalDict(items)
else:
for i, v in self._storage.items():
if key in i:
return v
raise KeyError(key)

def __setitem__(self, key, value):
interval = key if isinstance(key, Interval) else singleton(key)

if interval.empty:
return

new_items = []
found = False
for i, v in self._items:
for i, v in self._storage.items():
if value == v:
found = True
new_items.append((i | interval, v))
# Extend existing key
self._storage.pop(i)
self._storage[i | interval] = v
elif i.overlaps(interval):
# Reduce existing key
remaining = i - interval
self._storage.pop(i)
if not remaining.empty:
new_items.append((remaining, v))
else:
new_items.append((i, v))
self._storage[remaining] = v

if not found:
new_items.append((interval, value))

self._items = new_items
self._storage[interval] = value

def __delitem__(self, key):
interval = key if isinstance(key, Interval) else singleton(key)

if interval.empty:
return

new_items = []
found = False
for i, v in self._items:
for i, v in self._storage.items():
if i.overlaps(interval):
found = True
remaining = i - interval
self._storage.pop(i)
if not remaining.empty:
new_items.append((remaining, v))
else:
new_items.append((i, v))

self._items = new_items
self._storage[remaining] = v

if not found and not isinstance(key, Interval):
raise KeyError(key)

def __iter__(self):
return iter(self.keys())
return iter(self._storage)

def __len__(self):
return len(self._items)
return len(self._storage)

def __contains__(self, key):
return key in self.domain()
Expand All @@ -307,6 +311,6 @@ def __repr__(self):

def __eq__(self, other):
if isinstance(other, IntervalDict):
return self.items() == other.items()
return self.as_dict() == other.as_dict()
else:
return NotImplemented
6 changes: 4 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setup(
name='portion',
version='2.0.2',
version='2.1.0',
license='LGPLv3',

author='Alexandre Decan',
Expand Down Expand Up @@ -43,7 +43,9 @@
packages=find_packages(include=['portion']),
python_requires='~=3.5',

install_requires=[],
install_requires=[
'sortedcontainers ~= 2.2.2',
],
extras_require={
'test': ['pytest ~= 5.0.1'],
'travis': ['coverage ~= 5.0.3', 'coveralls ~= 1.11.1']
Expand Down

0 comments on commit 66f0264

Please sign in to comment.