Repository: benmaier/binpacking

Issue:
Lambda key for lists
It would be improve usability if we could pass in a lambda to retrieve the weight. The usage would be something like

```
a = [{'name': 'foo', 'weight': 30},
     {'name': 'bar', 'weight': 10}]
binpacking.to_constant_volume(a, key=lambda x: x['weight'], 30)
```

This would also eliminated the need for a weight_pos variable since it can then be written as

```
a = [('foo', 30), ('bar', 10)]
binpacking.to_constant_volume(a, key=lambda x: x[1], 30)
```
  

Discussion:

Patch:
diff --git a/binpacking/to_constant_bin_number.py b/binpacking/to_constant_bin_number.py
index dee0746..d189a3e 100644
--- a/binpacking/to_constant_bin_number.py
+++ b/binpacking/to_constant_bin_number.py
@@ -15,7 +15,7 @@ def csv_to_constant_bin_number(filepath,weight_column,N_bin,has_header=False,del
     save_csvs(bins,filepath,header,delim=delim,quotechar=quotechar)
 
 
-def to_constant_bin_number(d,N_bin,weight_pos=None,lower_bound=None,upper_bound=None):
+def to_constant_bin_number(d,N_bin,weight_pos=None,key=None,lower_bound=None,upper_bound=None):
     '''
     Distributes a list of weights, a dictionary of weights or a list of tuples containing weights
     to a fixed number of bins while trying to keep the weight distribution constant.
@@ -27,6 +27,7 @@ def to_constant_bin_number(d,N_bin,weight_pos=None,lower_bound=None,upper_bound=
          
     optional:
     ~~~ weight_pos: int -- if d is a list of tuples, this integer number gives the position of the weight in a tuple
+    ~~~ key: function -- if d is a list, this key functions grabs the weight for an item
     ~~~ lower_bound: weights under this bound are not considered
     ~~~ upper_bound: weights exceeding this bound are not considered
     '''
@@ -44,17 +45,21 @@ def to_constant_bin_number(d,N_bin,weight_pos=None,lower_bound=None,upper_bound=
         raise Exception("lower_bound is greater or equal to upper_bound")
     
     isdict = isinstance(d,dict)
-    is_tuple_list = not isdict and hasattr(d[0],'__len__')
 
-    if is_tuple_list:
+    if isinstance(d, list) and hasattr(d[0], '__len__'):
         if weight_pos is not None:
-
-            new_dict = { i: tup for i,tup in enumerate(d) }
-            d = { i: tup[weight_pos] for i,tup in enumerate(d) }
-            isdict = True
-        else:
-            raise Exception("no weight axis provided for tuple list")
-
+            key = lambda x: x[weight_pos]
+        if key is None:
+            raise ValueError("Must provide weight_pos or key for tuple list")
+    
+    if isinstance(d, list) and key:
+        new_dict = {i: val for i, val in enumerate(d)}
+        print(new_dict)
+        d = {i: key(val) for i, val in enumerate(d)}
+        isdict = True
+        is_tuple_list = True
+    else:
+        is_tuple_list = False
 
     if isdict:
 
diff --git a/binpacking/to_constant_volume.py b/binpacking/to_constant_volume.py
index 8b6002a..aa8c7b1 100644
--- a/binpacking/to_constant_volume.py
+++ b/binpacking/to_constant_volume.py
@@ -15,7 +15,7 @@ def csv_to_constant_volume(filepath,weight_column,V_max,has_header=False,delim='
     save_csvs(bins,filepath,header,delim=delim,quotechar=quotechar)
 
 
-def to_constant_volume(d,V_max,weight_pos=None,lower_bound=None,upper_bound=None):
+def to_constant_volume(d,V_max,weight_pos=None,key=None,lower_bound=None,upper_bound=None):
     '''
     Distributes a list of weights, a dictionary of weights or a list of tuples containing weights
     to a minimal number of bins which have a fixed volume.
@@ -27,6 +27,7 @@ def to_constant_volume(d,V_max,weight_pos=None,lower_bound=None,upper_bound=None
          
     optional:
     ~~~ weight_pos: int -- if d is a list of tuples, this integer number gives the position of the weight in a tuple
+    ~~~ key: function -- if d is a list, this key functions grabs the weight for an item
     ~~~ lower_bound: weights under this bound are not considered
     ~~~ upper_bound: weights exceeding this bound are not considered
     '''
@@ -44,16 +45,21 @@ def to_constant_volume(d,V_max,weight_pos=None,lower_bound=None,upper_bound=None
         raise Exception("lower_bound is greater or equal to upper_bound")
 
     isdict = isinstance(d,dict)
-    is_tuple_list = (not isdict) and (hasattr(d[0],'__len__'))
 
-    if is_tuple_list:
+    if isinstance(d, list) and hasattr(d[0], '__len__'):
         if weight_pos is not None:
-
-            new_dict = { i: tup for i,tup in enumerate(d) }
-            d = { i: tup[weight_pos] for i,tup in enumerate(d) }
-            isdict = True
-        else:
-            raise Exception("no weight axis provided for tuple list")
+            key = lambda x: x[weight_pos]
+        if key is None:
+            raise ValueError("Must provide weight_pos or key for tuple list")
+    
+    if isinstance(d, list) and key:
+        new_dict = {i: val for i, val in enumerate(d)}
+        print(new_dict)
+        d = {i: key(val) for i, val in enumerate(d)}
+        isdict = True
+        is_tuple_list = True
+    else:
+        is_tuple_list = False
 
     if isdict:
 


Test patch:
diff --git a/binpacking/tests/constant_bin_number.py b/binpacking/tests/constant_bin_number.py
index 298f6c8..19ca943 100644
--- a/binpacking/tests/constant_bin_number.py
+++ b/binpacking/tests/constant_bin_number.py
@@ -5,3 +5,28 @@ def test_only_zero_weights():
     values = [0, 0, 0]
     bins = to_constant_bin_number(values, 4)
     assert bins == [[0, 0, 0], [], [], []]
+
+def test_weight_pos():
+    values = [
+        [1, 'x'],
+        [2, 'y'],
+        [1, 'z'],
+    ]
+    bins = to_constant_bin_number(values, 2, weight_pos=0)
+    for bin_ in bins:
+        for item in bin_:
+            assert isinstance(item[0], int)
+            assert isinstance(item[1], str)
+
+def test_key_func():
+    values = [
+        {'x': 'a', 'y': 1},
+        {'x': 'b', 'y': 5},
+        {'x': 'b', 'y': 3},
+    ]
+    bins = to_constant_bin_number(values, 2, key=lambda x: x['y'])
+
+    for bin_ in bins:
+        for item in bin_:
+            assert 'x' in item
+            assert 'y' in item
\ No newline at end of file
diff --git a/binpacking/tests/constant_volume.py b/binpacking/tests/constant_volume.py
index d549bac..4026f53 100644
--- a/binpacking/tests/constant_volume.py
+++ b/binpacking/tests/constant_volume.py
@@ -4,4 +4,29 @@ from binpacking.to_constant_volume import to_constant_volume, csv_to_constant_vo
 def test_exact_fit():
     values = [1, 2, 1]
     bins = to_constant_volume(values, 2)
-    assert len(bins) == 2
\ No newline at end of file
+    assert len(bins) == 2
+
+def test_weight_pos():
+    values = [
+        [1, 'x'],
+        [2, 'y'],
+        [1, 'z'],
+    ]
+    bins = to_constant_volume(values, 2, weight_pos=0)
+    for bin_ in bins:
+        for item in bin_:
+            assert isinstance(item[0], int)
+            assert isinstance(item[1], str)
+
+def test_key_func():
+    values = [
+        {'x': 'a', 'y': 1},
+        {'x': 'b', 'y': 5},
+        {'x': 'b', 'y': 3},
+    ]
+    bins = to_constant_volume(values, 2, key=lambda x: x['y'])
+
+    for bin_ in bins:
+        for item in bin_:
+            assert 'x' in item
+            assert 'y' in item
\ No newline at end of file