Skip to content

Commit

Permalink
Add missing parameter in combine
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexandreDecan committed Sep 15, 2024
1 parent 7cf9adf commit 5d6a176
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 10 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Changelog


## Next release
## Next release (2.5.0)

### Added
- The `combine` method of an `IntervalDict` accepts a `missing` parameter to fill values for non-overlapping keys (see [#95](https://github.com/AlexandreDecan/portion/issues/95)).

### Changed
- Drop official support for Python 3.7.
Expand All @@ -11,7 +14,7 @@
## 2.4.2 (2023-12-06)

### Fixed
- Import error when using `create_api` in Python 3.10+ (see [#87](https://github.com/AlexandreDecan/portion/issues/85)).
- Import error when using `create_api` in Python 3.10+ (see [#87](https://github.com/AlexandreDecan/portion/issues/87)).



Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,9 +697,11 @@ See [#44](https://github.com/AlexandreDecan/portion/issues/44#issuecomment-71019

Two `IntervalDict` instances can be combined together using the `.combine` method.
This method returns a new `IntervalDict` whose keys and values are taken from the two
source `IntervalDict`. Values corresponding to non-intersecting keys are simply copied,
while values corresponding to intersecting keys are combined together using the provided
function, as illustrated hereafter:
source `IntervalDict`.
The values corresponding to intersecting keys (i.e., when the two instances overlap)
are combined using the provided `how` function, while values corresponding to
non-intersecting keys are simply copied (i.e., the `how` function is not called
for them), as illustrated hereafter:

```python
>>> d1 = P.IntervalDict({P.closed(0, 2): 'banana'})
Expand All @@ -710,6 +712,16 @@ function, as illustrated hereafter:

```

The `combine` method also accepts a `missing` parameter. When `missing` is set,
the `how` function is called even for non-intersecting keys, using the value of
`missing` to replace the missing values:

```python
>>> d1.combine(d2, how=concat, missing='kiwi')
{[0,1): 'banana/kiwi', [1,2]: 'banana/orange', (2,3]: 'kiwi/orange'}

```

Resulting keys always correspond to an outer join. Other joins can be easily simulated
by querying the resulting `IntervalDict` as follows:

Expand All @@ -724,6 +736,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
Expand Down
19 changes: 15 additions & 4 deletions portion/dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,26 +218,37 @@ def update(self, mapping_or_iterable):
for i, v in data:
self[i] = v

def combine(self, other, how):
def combine(self, other, how, *, missing=...):
"""
Return a new IntervalDict that combines the values from current and
provided ones.
provided IntervalDict.
If d = d1.combine(d2, f), then d contains (1) all values from d1 whose
keys do not intersect the ones of d2, (2) all values from d2 whose keys
do not intersect the ones of d1, and (3) f(x, y) for x in d1, y in d2 for
intersecting keys.
When missing is set, the how function is called even for non-intersecting
keys using the value of missing to replace the missing values. This is,
case (1) corresponds to f(x, missing) and case (2) to f(missing, y).
:param other: another IntervalDict instance.
:param how: a function of two parameters that combines values.
:param missing: if set, use this value for missing values when calling "how".
:return: a new IntervalDict instance.
"""
new_items = []

dom1, dom2 = self.domain(), other.domain()

new_items.extend(self[dom1 - dom2].items())
new_items.extend(other[dom2 - dom1].items())
if missing is Ellipsis:
new_items.extend(self[dom1 - dom2].items())
new_items.extend(other[dom2 - dom1].items())
else:
for i, v in self[dom1 - dom2].items():
new_items.append((i, how(v, missing)))
for i, v in other[dom2 - dom1].items():
new_items.append((i, how(missing, v)))

intersection = dom1 & dom2
d1, d2 = self[intersection], other[intersection]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

setup(
name="portion",
version="2.4.2",
version="2.5.0",
license="LGPLv3",
author="Alexandre Decan",
url="https://github.com/AlexandreDecan/portion",
Expand Down
26 changes: 26 additions & 0 deletions tests/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def add(x, y): return x + y

def test_combine_nonempty(self):
def add(x, y): return x + y

d1 = P.IntervalDict([(P.closed(1, 3) | P.closed(5, 7), 1)])
d2 = P.IntervalDict([(P.closed(2, 4) | P.closed(6, 8), 2)])
assert d1.combine(d2, add) == d2.combine(d1, add)
Expand Down Expand Up @@ -180,6 +181,31 @@ def add(x, y): return x + y
P.openclosed(4, 5): 1,
})

def test_combine_missing(self):
def how(x, y): return x, y

d1 = P.IntervalDict([(P.closed(1, 3), 1)])
d2 = P.IntervalDict([(P.closed(2, 4), 2)])
assert d1.combine(d2, how=how, missing=None) == P.IntervalDict([
(P.closedopen(1, 2), (1, None)),
(P.closed(2, 3), (1, 2)),
(P.openclosed(3, 4), (None, 2))
])

assert d2.combine(d1, how=how, missing=None) == P.IntervalDict([
(P.closedopen(1, 2), (None, 1)),
(P.closed(2, 3), (2, 1)),
(P.openclosed(3, 4), (2, None))
])

def add(x, y): return x + y
assert d2.combine(d1, how=add, missing=3) == P.IntervalDict([
(P.closedopen(1, 2), 4),
(P.closed(2, 3), 3),
(P.openclosed(3, 4), 5)
])


def test_containment(self):
d = P.IntervalDict([(P.closed(0, 3), 0)])
assert 0 in d
Expand Down

0 comments on commit 5d6a176

Please sign in to comment.