Skip to content

Commit 3f31f2a

Browse files
authored
fix #522 and fix #533:
* 533 --> allowed to use 0 padding in axis labels * 522 --> fixed aggregation on arrays containing zero padded string labels (like '01', '02', ...)
1 parent d4d57d6 commit 3f31f2a

6 files changed

Lines changed: 79 additions & 37 deletions

File tree

‎doc/source/changes/version_0_28.rst.inc‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,11 @@ Miscellaneous improvements
344344

345345
* Renamed sheet_name of LArray.to_excel to sheet since it can also be an index (closes :issue:580).
346346

347+
* allowed to create axes with zero padded string labels (closes :issue:`533`):
348+
349+
>>> Axis('zero_padding=01,02,03,10,11,12')
350+
Axis(['01', '02', '03', '10', '11', '12'], 'zero_padding')
351+
347352

348353
Fixes
349354
-----
@@ -392,3 +397,13 @@ Fixes
392397
b0 0 3
393398
b1 1 4
394399
b2 2 5
400+
401+
* fixed aggregation on arrays containing zero padded string labels (closes :issue:`522`):
402+
403+
>>> arr = ndtest('zero_padding=01,02,03,10,11,12')
404+
>>> arr
405+
zero_padding 01 02 03 10 11 12
406+
0 1 2 3 4 5
407+
>>> arr.sum('01,02,03 >> 01_03; 10')
408+
zero_padding 01_03 10
409+
3 3

‎larray/core/axis.py‎

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -184,20 +184,7 @@ def labels(self, labels):
184184
labels = np.arange(length)
185185
iswildcard = True
186186
else:
187-
# TODO: move this to _to_ticks????
188-
# we convert to an ndarray to save memory for scalar ticks (for
189-
# LGroup ticks, it does not make a difference since a list of LGroup
190-
# and an ndarray of LGroup are both arrays of pointers)
191-
ticks = _to_ticks(labels, parse_single_int=True)
192-
if _contain_group_ticks(ticks):
193-
# avoid getting a 2d array if all LGroup have the same length
194-
labels = np.empty(len(ticks), dtype=object)
195-
# this does not work if some values have a length (with a valid __len__) and others not
196-
# labels[:] = ticks
197-
for i, tick in enumerate(ticks):
198-
labels[i] = tick
199-
else:
200-
labels = np.asarray(ticks)
187+
labels = _to_ticks(labels, parse_single_int=True)
201188
length = len(labels)
202189
iswildcard = False
203190

‎larray/core/group.py‎

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ def _range_str_to_range(s, stack_depth=1):
223223
>>> list(_range_str_to_range('2..6 step 2'))
224224
[2, 4, 6]
225225
226+
0 padding
227+
>>> list(_range_str_to_range('01..12'))
228+
['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
229+
226230
any special character except . and spaces should work
227231
>>> _range_str_to_range('a|+*@-b .. a|+*@-d')
228232
['a|+*@-b', 'a|+*@-c', 'a|+*@-d']
@@ -236,6 +240,8 @@ def _range_str_to_range(s, stack_depth=1):
236240
if stop is None:
237241
raise ValueError("no stop bound provided in range: %r" % s)
238242
stop = _parse_bound(stop, stack_depth + 1)
243+
if isinstance(start, basestring) or isinstance(stop, basestring):
244+
start, stop = str(start), str(stop)
239245
# TODO: use parse_bound
240246
step = int(step) if step is not None else 1
241247
return generalized_range(start, stop, step)
@@ -365,6 +371,7 @@ def _to_tick(v):
365371
return str(v)
366372

367373

374+
# TODO: remove the conversion to list in doctests once Python 2 is dropped
368375
def _to_ticks(s, parse_single_int=False):
369376
"""
370377
Makes a (list of) value(s) usable as the collection of labels for an Axis (ie hashable).
@@ -378,53 +385,52 @@ def _to_ticks(s, parse_single_int=False):
378385
379386
Returns
380387
-------
381-
collection of labels
382-
383-
Notes
384-
-----
385-
This function is only used in Axis.__init__ and union().
388+
Numpy 1D array containing labels
386389
387390
Examples
388391
--------
389-
>>> _to_ticks('M , F')
392+
>>> list(_to_ticks('M , F'))
390393
['M', 'F']
391-
>>> _to_ticks('A,C..E,F..G,Z')
394+
>>> list(_to_ticks('A,C..E,F..G,Z'))
392395
['A', 'C', 'D', 'E', 'F', 'G', 'Z']
393-
>>> _to_ticks('U')
396+
>>> list(_to_ticks('U'))
394397
['U']
395398
>>> list(_to_ticks('..3'))
396399
[0, 1, 2, 3]
400+
>>> list(_to_ticks('01..12'))
401+
['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
402+
>>> list(_to_ticks('01,02,03,10,11,12'))
403+
['01', '02', '03', '10', '11', '12']
397404
"""
398405
if isinstance(s, ABCAxis):
399406
return s.labels
400407
if isinstance(s, Group):
401408
# a single LGroup used for all ticks of an Axis
402409
return _to_ticks(s.eval())
403-
elif isinstance(s, pd.Index):
404-
return s.values
405410
elif isinstance(s, np.ndarray):
406411
# we assume it has already been translated
407412
# XXX: Is it a safe assumption?
408413
return s
414+
415+
if isinstance(s, pd.Index):
416+
ticks = s.values
409417
elif isinstance(s, (list, tuple)):
410-
return [_to_tick(e) for e in s]
418+
ticks = [_to_tick(e) for e in s]
411419
elif sys.version >= '3' and isinstance(s, range):
412-
return list(s)
420+
ticks = s
413421
elif isinstance(s, basestring):
414422
seq = _seq_str_to_seq(s, parse_single_int=parse_single_int)
415423
if isinstance(seq, slice):
416424
raise ValueError("using : to define axes is deprecated, please use .. instead")
417-
elif isinstance(seq, (basestring, int)):
418-
return [seq]
419-
else:
420-
return seq
425+
ticks = [seq] if isinstance(seq, (basestring, int)) else seq
421426
elif hasattr(s, '__array__'):
422-
return s.__array__()
427+
ticks = s.__array__()
423428
else:
424429
try:
425-
return list(s)
430+
ticks = list(s)
426431
except TypeError:
427432
raise TypeError("ticks must be iterable (%s is not)" % type(s))
433+
return np.asarray(ticks)
428434

429435

430436
_axis_name_pattern = re.compile('\s*(([A-Za-z]\w*)(\.i)?\s*\[)?(.*)')

‎larray/tests/test_array.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@
2424

2525
class TestValueStrings(TestCase):
2626
def test_split(self):
27-
self.assertEqual(_to_ticks('M,F'), ['M', 'F'])
28-
self.assertEqual(_to_ticks('M, F'), ['M', 'F'])
27+
assert_array_equal(_to_ticks('M,F'), np.asarray(['M', 'F']))
28+
assert_array_equal(_to_ticks('M, F'), np.asarray(['M', 'F']))
2929

3030
def test_union(self):
3131
self.assertEqual(union('A11,A22', 'A12,A22'), ['A11', 'A22', 'A12'])
3232

3333
def test_range(self):
34-
self.assertEqual(_to_ticks('0..115'), range(116))
35-
self.assertEqual(_to_ticks('..115'), range(116))
34+
assert_array_equal(_to_ticks('0..115'), np.asarray(range(116)))
35+
assert_array_equal(_to_ticks('..115'), np.asarray(range(116)))
3636
with self.assertRaises(ValueError):
3737
_to_ticks('10..')
3838
with self.assertRaises(ValueError):
@@ -1654,6 +1654,11 @@ def test_group_agg_anonymous_axis(self):
16541654
raw = np.asarray(la)
16551655
assert_array_equal(la.sum(a2[0, 2]), raw[:, [0, 2]].sum(1))
16561656

1657+
def test_group_agg_zero_padded_label(self):
1658+
arr = ndtest("a=01,02,03,10,11; b=b0..b2")
1659+
expected = LArray([36, 30, 39], "a=01_03,10,11")
1660+
assert_array_equal(arr.sum("01,02,03 >> 01_03; 10; 11", "b"), expected)
1661+
16571662
def test_group_agg_on_int_array(self):
16581663
# issue 193
16591664
arr = ndtest('year=2014..2018')

‎larray/tests/test_axis.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ def test_init(self):
3838
# range-string
3939
axis = Axis('0..115', 'age')
4040
assert_array_equal(axis.labels, np.arange(116))
41+
# int-like labels with 0 padding
42+
assert_array_equal(Axis('01..12', 'zero_padding').labels, [str(i).zfill(2) for i in range(1, 13)])
43+
assert_array_equal(Axis('01,02,03,10,11,12', 'zero_padding').labels, ['01', '02', '03', '10', '11', '12'])
44+
4145
# another axis group
4246
group = axis[:10]
4347
group_axis = Axis(group)

‎larray/util/misc.py‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,30 @@ def error_handler(error, flag):
516516

517517

518518
def _isintstring(s):
519-
return s.isdigit() or (len(s) > 1 and s[0] == '-' and s[1:].isdigit())
519+
"""
520+
Return True if the passed string represents an integer.
521+
Zero padded integers are considered as strings and not integers.
522+
523+
Parameters
524+
----------
525+
s : str
526+
string to test if representing an integer.
527+
528+
Examples
529+
--------
530+
>>> _isintstring('12')
531+
True
532+
>>> _isintstring('-12')
533+
True
534+
>>> _isintstring('a1')
535+
False
536+
>>> _isintstring('01')
537+
False
538+
"""
539+
def isposint(s):
540+
# exclude zero padded strings
541+
return s.isdigit() and not (len(s) > 1 and s[0] == '0')
542+
return isposint(s) or (len(s) > 1 and s[0] == '-' and isposint(s[1:]))
520543

521544

522545
def _parse_bound(s, stack_depth=1, parse_int=True):
@@ -545,6 +568,8 @@ def _parse_bound(s, stack_depth=1, parse_int=True):
545568
2
546569
>>> _parse_bound('42')
547570
42
571+
>>> _parse_bound('01')
572+
'01'
548573
"""
549574
s = s.strip()
550575
if s == '':

0 commit comments

Comments
 (0)