Coverage for src/thunderlab/tabledata.py: 89%
1560 statements
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-09 16:02 +0000
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-09 16:02 +0000
1"""
2Tables with hierarchical headers and units
4## Classes
6- `class TableData`: tables with a rich hierarchical header
7 including units and column-specific formats. Kind of similar to a
8 pandas data frame, but with intuitive numpy-style indexing and nicely
9 formatted output to csv, html, and latex.
12## Helper functions
14- `write()`: shortcut for constructing and writing a TableData.
15- `latex_unit()`: translate unit string into SIunit LaTeX code.
16- `index2aa()`: convert an integer into an alphabetical representation.
17- `aa2index()`: convert an alphabetical representation to an index.
20## Configuration
22- `add_write_table_config()`: add parameter specifying how to write a table to a file as a new section to a configuration.
23- `write_table_args()`: translates a configuration to the respective parameter names for writing a table to a file.
24"""
26import sys
27import os
28import re
29import math as m
30import numpy as np
31from io import StringIO
32try:
33 import pandas as pd
34except ImportError:
35 pass
38__pdoc__ = {}
39__pdoc__['TableData.__contains__'] = True
40__pdoc__['TableData.__len__'] = True
41__pdoc__['TableData.__iter__'] = True
42__pdoc__['TableData.__next__'] = True
43__pdoc__['TableData.__setupkey__'] = True
44__pdoc__['TableData.__call__'] = True
45__pdoc__['TableData.__getitem__'] = True
46__pdoc__['TableData.__setitem__'] = True
47__pdoc__['TableData.__delitem__'] = True
48__pdoc__['TableData.__str__'] = True
51default_missing_str = '-'
52"""Default string indicating nan data elements when outputting data."""
54default_missing_inputs = ['na', 'NA', 'nan', 'NAN', '-']
55"""Default strings that are translated to nan when loading table data."""
58class TableData(object):
59 """Table with numpy-style indexing and a rich hierarchical header including units and formats.
61 Parameters
62 ----------
63 data: str, stream, ndarray
64 - a filename: load table from file with name `data`.
65 - a stream/file handle: load table from that stream.
66 - 1-D or 2-D ndarray of data: the data of the table.
67 Requires als a specified `header`.
68 header: list of str
69 Header labels for each column.
70 units: list of str, optional
71 Unit strings for each column.
72 formats: str or list of str, optional
73 Format strings for each column. If only a single format string is
74 given, then all columns are initialized with this format string.
75 missing: list of str
76 Missing data are indicated by one of these strings.
78 Manipulate table header
79 -----------------------
81 Each column of the table has a label (the name of the column), a
82 unit, and a format specifier. Sections group columns into a hierarchy.
84 - `__init__()`: initialize a TableData from data or a file.
85 - `append()`: append column to the table.
86 - `insert()`: insert a table column at a given position.
87 - `remove()`: remove columns from the table.
88 - `section()`: the section name of a specified column.
89 - `set_section()`: set a section name.
90 - `append_section()`: add sections to the table header.
91 - `insert_section()`: insert a section at a given position of the table header.
92 - `label()`: the name of a column.
93 - `set_label()`: set the name of a column.
94 - `unit()`: the unit of a column.
95 - `set_unit()`: set the unit of a column.
96 - `set_units()`: set the units of all columns.
97 - `format()`: the format string of the column.
98 - `set_format()`: set the format string of a column.
99 - `set_formats()`: set the format strings of all columns.
101 For example:
102 ```
103 tf = TableData('data.csv')
104 ```
105 loads a table directly from a file. See `load()` for details.
106 ```
107 tf = TableData(np.random.randn(4,3), header=['aaa', 'bbb', 'ccc'], units=['m', 's', 'g'], formats='%.2f')
108 ```
109 results in
110 ``` plain
111 aaa bbb ccc
112 m s g
113 1.45 0.01 0.16
114 -0.74 -0.58 -1.34
115 -2.06 0.08 1.47
116 -0.43 0.60 1.38
117 ```
119 A more elaborate way to construct a table is:
120 ```
121 df = TableData()
122 # first column with section names and 3 data values:
123 df.append(["data", "partial information", "size"], "m", "%6.2f",
124 [2.34, 56.7, 8.9])
125 # next columns with single data values:
126 df.append("full weight", "kg", "%.0f", 122.8)
127 df.append_section("complete reaction")
128 df.append("speed", "m/s", "%.3g", 98.7)
129 df.append("median jitter", "mm", "%.1f", 23)
130 df.append("size", "g", "%.2e", 1.234)
131 # add a missing value to the second column:
132 df.append_data(np.nan, 1)
133 # fill up the remaining columns of the row:
134 df.append_data((0.543, 45, 1.235e2))
135 # append data to the next row starting at the second column:
136 df.append_data((43.21, 6789.1, 3405, 1.235e-4), 1) # next row
137 ```
138 results in
139 ``` plain
140 data
141 partial information complete reaction
142 size full weight speed median jitter size
143 m kg m/s mm g
144 2.34 123 98.7 23.0 1.23e+00
145 56.70 - 0.543 45.0 1.24e+02
146 8.90 43 6.79e+03 3405.0 1.23e-04
147 ```
149 Table columns
150 -------------
152 Columns can be specified by an index or by the name of a column. In
153 table headers with sections the colum can be specified by the
154 section names and the column name separated by '>'.
156 - `index()`: the index of a column.
157 - `__contains__()`: check for existence of a column.
158 - `find_col()`: find the start and end index of a column specification.
159 - `column_spec()`: full specification of a column with all its section names.
160 - `column_head()`: the name, unit, and format of a column.
161 - `table_header()`: the header of the table without content.
163 For example:
164 ```
165 df.index('complete reaction>size) # returns 4
166 'speed' in df # is True
167 ```
169 Iterating over columns
170 ----------------------
172 A table behaves like an ordered dictionary with column names as
173 keys and the data of each column as values.
174 Iterating over a table goes over columns.
176 - `keys()`: list of unique column keys for all available columns.
177 - `values()`: list of column data corresponding to keys().
178 - `items()`: list of tuples with unique column specifications and the corresponding data.
179 - `__len__()`: the number of columns.
180 - `__iter__()`: initialize iteration over data columns.
181 - `__next__()`: return data of next column as a list.
182 - `data`: the table data as a list over columns each containing a list of data elements.
184 For example:
185 ```
186 print('column specifications:')
187 for c in range(df.columns()):
188 print(df.column_spec(c))
189 print('keys():')
190 for c, k in enumerate(df.keys()):
191 print('%d: %s' % (c, k))
192 print('values():')
193 for c, v in enumerate(df.values()):
194 print(v)
195 print('iterating over the table:')
196 for v in df:
197 print(v)
198 ```
199 results in
200 ``` plain
201 column specifications:
202 data>partial information>size
203 data>partial information>full weight
204 data>complete reaction>speed
205 data>complete reaction>median jitter
206 data>complete reaction>size
207 keys():
208 0: data>partial information>size
209 1: data>partial information>full weight
210 2: data>complete reaction>speed
211 3: data>complete reaction>median jitter
212 4: data>complete reaction>size
213 values():
214 [2.34, 56.7, 8.9]
215 [122.8, nan, 43.21]
216 [98.7, 0.543, 6789.1]
217 [23, 45, 3405]
218 [1.234, 123.5, 0.0001235]
219 iterating over the table:
220 [2.34, 56.7, 8.9]
221 [122.8, nan, 43.21]
222 [98.7, 0.543, 6789.1]
223 [23, 45, 3405]
224 [1.234, 123.5, 0.0001235]
225 ```
227 Accessing data
228 --------------
230 In contrast to the iterator functions the [] operator treats the
231 table as a 2D-array where the first index indicates the row and
232 the second index the column.
234 Like a numpy aray the table can be sliced, and logical indexing can
235 be used to select specific parts of the table.
237 As for any function, columns can be specified as indices or strings.
239 - `rows()`: the number of rows.
240 - `columns()`: the number of columns.
241 - `shape`: number of rows and columns.
242 - `row()`: a single row of the table as TableData.
243 - `row_dict()`: a single row of the table as dictionary.
244 - `col()`: a single column of the table as TableData.
245 - `__call__()`: a single column of the table as ndarray.
246 - `__getitem__()`: data elements specified by slice.
247 - `__setitem__()`: assign values to data elements specified by slice.
248 - `__delitem__()`: delete data elements or whole columns or rows.
249 - `array()`: the table data as a ndarray.
250 - `data_frame()`: the table data as a pandas DataFrame.
251 - `dicts()`: the table as a list of dictionaries.
252 - `dict()`: the table as a dictionary.
253 - `append_data()`: append data elements to successive columns.
254 - `append_data_column()`: append data elements to a column.
255 - `set_column()`: set the column where to add data.
256 - `fill_data()`: fill up all columns with missing data.
257 - `clear_data()`: clear content of the table but keep header.
258 - `key_value()`: a data element returned as a key-value pair.
260 - `sort()`: sort the table rows in place.
261 - `statistics()`: descriptive statistics of each column.
263 For example:
264 ```
265 # single column:
266 df('size') # data of 'size' column as ndarray
267 df[:,'size'] # data of 'size' column as ndarray
268 df.col('size') # table with the single column 'size'
270 # single row:
271 df[2,:] # table with data of only the third row
272 df.row(2) # table with data of only the third row
274 # slices:
275 df[2:5,['size','jitter']] # sub-table
276 df[2:5,['size','jitter']].array() # ndarray with data only
278 # logical indexing:
279 df[df('speed') > 100.0, 'size'] = 0.0 # set size to 0 if speed is > 100
281 # delete:
282 del df[3:6, 'weight'] # delete rows 3-6 from column 'weight'
283 del df[3:5,:] # delete rows 3-5 completeley
284 del df[:,'speed'] # remove column 'speed' from table
285 df.remove('weight') # remove column 'weigth' from table
287 # sort and statistics:
288 df.sort(['weight', 'jitter'])
289 df.statistics()
290 ```
291 statistics() returns a table with standard descriptive statistics:
292 ``` plain
293 statistics data
294 - partial information complete reaction
295 - size full weight speed median jitter size
296 - m kg m/s mm g
297 mean 22.65 83 2.3e+03 1157.7 4.16e+01
298 std 24.23 40 3.18e+03 1589.1 5.79e+01
299 min 2.34 43 0.543 23.0 1.23e-04
300 quartile1 5.62 83 49.6 34.0 6.17e-01
301 median 8.90 123 98.7 45.0 1.23e+00
302 quartile3 32.80 - 3.44e+03 1725.0 6.24e+01
303 max 56.70 123 6.79e+03 3405.0 1.24e+02
304 count 3.00 2 3 3.0 3.00e+00
305 ```
307 Write and load tables
308 ---------------------
310 Table data can be written to a variety of text-based formats
311 including comma separated values, latex and html files. Which
312 columns are written can be controlled by the hide() and show()
313 functions. TableData can be loaded from all the written file formats
314 (except html), also directly via the constructor.
316 - `hide()`: hide a column or a range of columns.
317 - `hide_all()`: hide all columns.
318 - `hide_empty_columns()`: hide all columns that do not contain data.
319 - `show()`: show a column or a range of columns.
320 - `write()`: write table to a file or stream.
321 - `write_file_stream()`: write table to file or stream and return appropriate file name.
322 - `__str__()`: write table to a string.
323 - `load()`: load table from file or stream.
324 - `formats`: list of supported file formats for writing.
325 - `descriptions`: dictionary with descriptions of the supported file formats.
326 - `extensions`: dictionary with default filename extensions for each of the file formats.
327 - `ext_formats`: dictionary mapping filename extensions to file formats.
329 See documentation of the `write()` function for examples of the supported file formats.
331 """
333 formats = ['dat', 'ascii', 'csv', 'rtai', 'md', 'tex', 'html']
334 """list of strings: Supported output formats."""
335 descriptions = {'dat': 'data text file', 'ascii': 'ascii-art table',
336 'csv': 'comma separated values', 'rtai': 'rtai-style table',
337 'md': 'markdown', 'tex': 'latex tabular',
338 'html': 'html markup'}
339 """dict: Decription of output formats corresponding to `formats`."""
340 extensions = {'dat': 'dat', 'ascii': 'txt', 'csv': 'csv', 'rtai': 'dat',
341 'md': 'md', 'tex': 'tex', 'html': 'html'}
342 """dict: Default file extensions for the output `formats`. """
343 ext_formats = {'dat': 'dat', 'DAT': 'dat', 'txt': 'dat', 'TXT': 'dat',
344 'csv': 'csv', 'CSV': 'csv', 'md': 'md', 'MD': 'md',
345 'tex': 'tex', 'TEX': 'tex', 'html': 'html', 'HTML': 'html'}
346 """dict: Mapping of file extensions to the output formats."""
348 def __init__(self, data=None, header=None, units=None, formats=None,
349 missing=default_missing_inputs, stop=None):
350 self.data = []
351 self.shape = (0, 0)
352 self.header = []
353 self.nsecs = 0
354 self.units = []
355 self.formats = []
356 self.hidden = []
357 self.setcol = 0
358 self.addcol = 0
359 if header is not None:
360 if units is None:
361 units = ['']*len(header)
362 if formats is None:
363 formats = ['%g']*len(header)
364 elif not isinstance(formats, (list, tuple, np.ndarray)):
365 formats = [formats]*len(header)
366 for h, u, f in zip(header, units, formats):
367 self.append(h, u, f)
368 if data is not None:
369 if isinstance(data, TableData):
370 self.shape = data.shape
371 self.nsecs = data.nsecs
372 self.setcol = data.setcol
373 self.addcol = data.addcol
374 for c in range(data.columns()):
375 self.header.append([])
376 for h in data.header[c]:
377 self.header[c].append(h)
378 self.units.append(data.units[c])
379 self.formats.append(data.formats[c])
380 self.hidden.append(data.hidden[c])
381 self.data.append([])
382 for d in data.data[c]:
383 self.data[c].append(d)
384 elif isinstance(data, (list, tuple, np.ndarray)):
385 if isinstance(data[0], (list, tuple, np.ndarray)):
386 # 2D list, rows first:
387 for row in data:
388 for c, val in enumerate(row):
389 self.data[c].append(val)
390 else:
391 # 1D list:
392 for c, val in enumerate(data):
393 self.data[c].append(val)
394 else:
395 self.load(data, missing, stop)
397 def append(self, label, unit=None, formats=None, value=None,
398 fac=None, key=None):
399 """Append column to the table.
401 Parameters
402 ----------
403 label: str or list of str
404 Optional section titles and the name of the column.
405 unit: str or None
406 The unit of the column contents.
407 formats: str or None
408 The C-style format string used for printing out the column content, e.g.
409 '%g', '%.2f', '%s', etc.
410 If None, the format is set to '%g'.
411 value: None, float, int, str, etc. or list thereof, or list of dict
412 If not None, data for the column.
413 If list of dictionaries, extract from each dictionary in the list
414 the value specified by `key`. If `key` is `None` use `label` as
415 the key.
416 fac: float
417 If not None, multiply the data values by this number.
418 key: None or key of a dictionary
419 If not None and `value` is a list of dictionaries,
420 extract from each dictionary in the list the value specified
421 by `key` and assign the resulting list as data to the column.
423 Returns
424 -------
425 index: int
426 The index of the new column.
427 """
428 if self.addcol >= len(self.data):
429 if isinstance(label, (list, tuple, np.ndarray)):
430 self.header.append(list(reversed(label)))
431 label = label[-1]
432 else:
433 self.header.append([label])
434 self.formats.append(formats or '%g')
435 self.units.append(unit or '')
436 self.hidden.append(False)
437 self.data.append([])
438 if self.nsecs < len(self.header[-1])-1:
439 self.nsecs = len(self.header[-1])-1
440 else:
441 if isinstance(label, (list, tuple, np.ndarray)):
442 self.header[self.addcol] = list(reversed(label)) + self.header[self.addcol]
443 label = label[-1]
444 else:
445 self.header[self.addcol] = [label] + self.header[self.addcol]
446 self.units[self.addcol] = unit or ''
447 self.formats[self.addcol] = formats or '%g'
448 if self.nsecs < len(self.header[self.addcol])-1:
449 self.nsecs = len(self.header[self.addcol])-1
450 if not key:
451 key = label
452 if value is not None:
453 if isinstance(value, (list, tuple, np.ndarray)):
454 if key and len(value) > 0 and isinstance(value[0], dict):
455 value = [d[key] if key in d else float('nan') for d in value]
456 self.data[-1].extend(value)
457 else:
458 self.data[-1].append(value)
459 if fac:
460 for k in range(len(self.data[-1])):
461 self.data[-1][k] *= fac
462 self.addcol = len(self.data)
463 self.shape = (self.rows(), self.columns())
464 return self.addcol-1
466 def insert(self, column, label, unit=None, formats=None, value=None):
467 """Insert a table column at a given position.
469 .. WARNING::
470 If no `value` is given, the inserted column is an empty list.
472 Parameters
473 ----------
474 columns int or str
475 Column before which to insert the new column.
476 Column can be specified by index or name,
477 see `index()` for details.
478 label: str or list of str
479 Optional section titles and the name of the column.
480 unit: str or None
481 The unit of the column contents.
482 formats: str or None
483 The C-style format string used for printing out the column content, e.g.
484 '%g', '%.2f', '%s', etc.
485 If None, the format is set to '%g'.
486 value: None, float, int, str, etc. or list thereof
487 If not None, data for the column.
489 Returns
490 -------
491 index: int
492 The index of the inserted column.
494 Raises
495 ------
496 IndexError:
497 If an invalid column was specified.
498 """
499 col = self.index(column)
500 if col is None:
501 if isinstance(column, (np.integer, int)):
502 column = '%d' % column
503 raise IndexError('Cannot insert before non-existing column ' + column)
504 if isinstance(label, (list, tuple, np.ndarray)):
505 self.header.insert(col, list(reversed(label)))
506 else:
507 self.header.insert(col, [label])
508 self.formats.insert(col, formats or '%g')
509 self.units.insert(col, unit or '')
510 self.hidden.insert(col, False)
511 self.data.insert(col, [])
512 if self.nsecs < len(self.header[col])-1:
513 self.nsecs = len(self.header[col])-1
514 if value is not None:
515 if isinstance(value, (list, tuple, np.ndarray)):
516 self.data[col].extend(value)
517 else:
518 self.data[col].append(value)
519 self.addcol = len(self.data)
520 self.shape = (self.rows(), self.columns())
521 return col
523 def remove(self, columns):
524 """Remove columns from the table.
526 Parameters
527 -----------
528 columns: int or str or list of int of str
529 Columns can be specified by index or name,
530 see `index()` for details.
532 Raises
533 ------
534 IndexError:
535 If an invalid column was specified.
536 """
537 # fix columns:
538 if not isinstance(columns, (list, tuple, np.ndarray)):
539 columns = [ columns ]
540 if not columns:
541 return
542 # remove:
543 for col in columns:
544 c = self.index(col)
545 if c is None:
546 if isinstance(col, (np.integer, int)):
547 col = '%d' % col
548 raise IndexError('Cannot remove non-existing column ' + col)
549 continue
550 if c+1 < len(self.header):
551 self.header[c+1].extend(self.header[c][len(self.header[c+1]):])
552 del self.header[c]
553 del self.units[c]
554 del self.formats[c]
555 del self.hidden[c]
556 del self.data[c]
557 if self.setcol >= len(self.data):
558 self.setcol = 0
559 self.shape = (self.rows(), self.columns())
561 def section(self, column, level):
562 """The section name of a specified column.
564 Parameters
565 ----------
566 column: None, int, or str
567 A specification of a column.
568 See self.index() for more information on how to specify a column.
569 level: int
570 The level of the section to be returned. The column label itself is level=0.
572 Returns
573 -------
574 name: str
575 The name of the section at the specified level containing
576 the column.
577 index: int
578 The column index that contains this section
579 (equal or smaller thant `column`).
581 Raises
582 ------
583 IndexError:
584 If `level` exceeds the maximum possible level.
585 """
586 if level < 0 or level > self.nsecs:
587 raise IndexError('Invalid section level')
588 column = self.index(column)
589 while len(self.header[column]) <= level:
590 column -= 1
591 return self.header[column][level], column
593 def set_section(self, label, column, level):
594 """Set a section name.
596 Parameters
597 ----------
598 label: str
599 The new name to be used for the section.
600 column: None, int, or str
601 A specification of a column.
602 See self.index() for more information on how to specify a column.
603 level: int
604 The level of the section to be set. The column label itself is level=0.
605 """
606 column = self.index(column)
607 self.header[column][level] = label
608 return column
610 def append_section(self, label):
611 """Add sections to the table header.
613 Each column of the table has a header label. Columns can be
614 grouped into sections. Sections can be nested arbitrarily.
616 Parameters
617 ----------
618 label: stri or list of str
619 The name(s) of the section(s).
621 Returns
622 -------
623 index: int
624 The column index where the section was appended.
625 """
626 if self.addcol >= len(self.data):
627 if isinstance(label, (list, tuple, np.ndarray)):
628 self.header.append(list(reversed(label)))
629 else:
630 self.header.append([label])
631 self.units.append('')
632 self.formats.append('')
633 self.hidden.append(False)
634 self.data.append([])
635 else:
636 if isinstance(label, (list, tuple, np.ndarray)):
637 self.header[self.addcol] = list(reversed(label)) + self.header[self.addcol]
638 else:
639 self.header[self.addcol] = [label] + self.header[self.addcol]
640 if self.nsecs < len(self.header[self.addcol]):
641 self.nsecs = len(self.header[self.addcol])
642 self.addcol = len(self.data)-1
643 self.shape = (self.rows(), self.columns())
644 return self.addcol
646 def insert_section(self, column, section):
647 """Insert a section at a given position of the table header.
649 Parameters
650 ----------
651 columns int or str
652 Column before which to insert the new section.
653 Column can be specified by index or name,
654 see `index()` for details.
655 section: str
656 The name of the section.
658 Returns
659 -------
660 index: int
661 The index of the column where the section was inserted.
663 Raises
664 ------
665 IndexError:
666 If an invalid column was specified.
667 """
668 col = self.index(column)
669 if col is None:
670 if isinstance(column, (np.integer, int)):
671 column = '%d' % column
672 raise IndexError('Cannot insert at non-existing column ' + column)
673 self.header[col].append(section)
674 if self.nsecs < len(self.header[col])-1:
675 self.nsecs = len(self.header[col])-1
676 return col
678 def label(self, column):
679 """The name of a column.
681 Parameters
682 ----------
683 column: None, int, or str
684 A specification of a column.
685 See self.index() for more information on how to specify a column.
687 Returns
688 -------
689 name: str
690 The column label.
691 """
692 column = self.index(column)
693 return self.header[column][0]
695 def set_label(self, label, column):
696 """Set the name of a column.
698 Parameters
699 ----------
700 label: str
701 The new name to be used for the column.
702 column: None, int, or str
703 A specification of a column.
704 See self.index() for more information on how to specify a column.
705 """
706 column = self.index(column)
707 self.header[column][0] = label
708 return column
710 def unit(self, column):
711 """The unit of a column.
713 Parameters
714 ----------
715 column: None, int, or str
716 A specification of a column.
717 See self.index() for more information on how to specify a column.
719 Returns
720 -------
721 unit: str
722 The unit.
723 """
724 column = self.index(column)
725 return self.units[column]
727 def set_unit(self, unit, column):
728 """Set the unit of a column.
730 Parameters
731 ----------
732 unit: str
733 The new unit to be used for the column.
734 column: None, int, or str
735 A specification of a column.
736 See self.index() for more information on how to specify a column.
737 """
738 column = self.index(column)
739 self.units[column] = unit
740 return column
742 def set_units(self, units):
743 """Set the units of all columns.
745 Parameters
746 ----------
747 units: list of str
748 The new units to be used.
749 """
750 for c, u in enumerate(units):
751 self.units[c] = u
753 def format(self, column):
754 """The format string of the column.
756 Parameters
757 ----------
758 column: None, int, or str
759 A specification of a column.
760 See self.index() for more information on how to specify a column.
762 Returns
763 -------
764 format: str
765 The format string.
766 """
767 column = self.index(column)
768 return self.formats[column]
770 def set_format(self, format, column):
771 """Set the format string of a column.
773 Parameters
774 ----------
775 format: str
776 The new format string to be used for the column.
777 column: None, int, or str
778 A specification of a column.
779 See self.index() for more information on how to specify a column.
780 """
781 column = self.index(column)
782 self.formats[column] = format
783 return column
785 def set_formats(self, formats):
786 """Set the format strings of all columns.
788 Parameters
789 ----------
790 formats: str or list of str
791 The new format strings to be used.
792 If only a single format is specified,
793 then all columns get the same format.
794 """
795 if isinstance(formats, (list, tuple, np.ndarray)):
796 for c, f in enumerate(formats):
797 self.formats[c] = f or '%g'
798 else:
799 for c in range(len(self.formats)):
800 self.formats[c] = formats or '%g'
802 def table_header(self):
803 """The header of the table without content.
805 Returns
806 -------
807 data: TableData
808 A TableData object with the same header but empty data.
809 """
810 data = TableData()
811 sec_indices = [-1] * self.nsecs
812 for c in range(self.columns()):
813 data.append(*self.column_head(c))
814 for l in range(self.nsecs):
815 s, i = self.section(c, l+1)
816 if i != sec_indices[l]:
817 data.header[-1].append(s)
818 sec_indices[l] = i
819 data.nsecs = self.nsecs
820 return data
822 def column_head(self, column):
823 """The name, unit, and format of a column.
825 Parameters
826 ----------
827 column: None, int, or str
828 A specification of a column.
829 See self.index() for more information on how to specify a column.
831 Returns
832 -------
833 name: str
834 The column label.
835 unit: str
836 The unit.
837 format: str
838 The format string.
839 """
840 column = self.index(column)
841 return self.header[column][0], self.units[column], self.formats[column]
843 def column_spec(self, column):
844 """Full specification of a column with all its section names.
846 Parameters
847 ----------
848 column: int or str
849 Specifies the column.
850 See self.index() for more information on how to specify a column.
852 Returns
853 -------
854 s: str
855 Full specification of the column by all its section names and its header name.
856 """
857 c = self.index(column)
858 fh = [self.header[c][0]]
859 for l in range(self.nsecs):
860 fh.append(self.section(c, l+1)[0])
861 return '>'.join(reversed(fh))
863 def find_col(self, column):
864 """Find the start and end index of a column specification.
866 Parameters
867 ----------
868 column: None, int, or str
869 A specification of a column.
870 See self.index() for more information on how to specify a column.
872 Returns
873 -------
874 c0: int or None
875 A valid column index or None that is specified by `column`.
876 c1: int or None
877 A valid column index or None of the column following the range specified
878 by `column`.
879 """
881 def find_column_indices(ss, si, minns, maxns, c0, strict=True):
882 if si >= len(ss):
883 return None, None, None, None
884 ns0 = 0
885 for ns in range(minns, maxns+1):
886 nsec = maxns-ns
887 if ss[si] == '':
888 si += 1
889 continue
890 for c in range(c0, len(self.header)):
891 if nsec < len(self.header[c]) and \
892 ( ( strict and self.header[c][nsec] == ss[si] ) or
893 ( not strict and ss[si] in self.header[c][nsec] ) ):
894 ns0 = ns
895 c0 = c
896 si += 1
897 if si >= len(ss):
898 c1 = len(self.header)
899 for c in range(c0+1, len(self.header)):
900 if nsec < len(self.header[c]):
901 c1 = c
902 break
903 return c0, c1, ns0, None
904 elif nsec > 0:
905 break
906 return None, c0, ns0, si
908 if column is None:
909 return None, None
910 if not isinstance(column, (np.integer, int)) and column.isdigit():
911 column = int(column)
912 if isinstance(column, (np.integer, int)):
913 if column >= 0 and column < len(self.header):
914 return column, column+1
915 else:
916 return None, None
917 # find column by header:
918 ss = column.rstrip('>').split('>')
919 maxns = self.nsecs
920 si0 = 0
921 while si0 < len(ss) and ss[si0] == '':
922 maxns -= 1
923 si0 += 1
924 if maxns < 0:
925 maxns = 0
926 c0, c1, ns, si = find_column_indices(ss, si0, 0, maxns, 0, True)
927 if c0 is None and c1 is not None:
928 c0, c1, ns, si = find_column_indices(ss, si, ns, maxns, c1, False)
929 return c0, c1
931 def index(self, column):
932 """The index of a column.
934 Parameters
935 ----------
936 column: None, int, or str
937 A specification of a column.
938 - None: no column is specified
939 - int: the index of the column (first column is zero), e.g. `index(2)`.
940 - a string representing an integer is converted into the column index,
941 e.g. `index('2')`
942 - a string specifying a column by its header.
943 Header names of descending hierarchy are separated by '>'.
945 Returns
946 -------
947 index: int or None
948 A valid column index or None.
949 """
950 c0, c1 = self.find_col(column)
951 return c0
953 def __contains__(self, column):
954 """Check for existence of a column.
956 Parameters
957 ----------
958 column: None, int, or str
959 The column to be checked.
960 See self.index() for more information on how to specify a column.
962 Returns
963 -------
964 contains: bool
965 True if `column` specifies an existing column key.
966 """
967 return self.index(column) is not None
969 def keys(self):
970 """List of unique column keys for all available columns.
972 Returns
973 -------
974 keys: list of str
975 List of unique column specifications.
976 """
977 return [self.column_spec(c) for c in range(self.columns())]
979 def values(self):
980 """List of column data corresponding to keys().
982 Returns
983 -------
984 data: list of list of values
985 The data of the table. First index is columns!
986 """
987 return self.data
989 def items(self):
990 """Column names and corresponding data.
992 Returns
993 -------
994 items: list of tuples
995 Unique column specifications and the corresponding data.
996 """
997 return [(self.column_spec(c), self.data[c]) for c in range(self.columns())]
999 def __len__(self):
1000 """The number of columns.
1002 Returns
1003 -------
1004 columns: int
1005 The number of columns contained in the table.
1006 """
1007 return self.columns()
1009 def __iter__(self):
1010 """Initialize iteration over data columns.
1011 """
1012 self.iter_counter = -1
1013 return self
1015 def __next__(self):
1016 """Next column of data.
1018 Returns
1019 -------
1020 data: list of values
1021 Table data of next column.
1022 """
1023 self.iter_counter += 1
1024 if self.iter_counter >= self.columns():
1025 raise StopIteration
1026 else:
1027 return self.data[self.iter_counter]
1029 def next(self):
1030 """Return next data columns. (python2 syntax)
1032 See also:
1033 ---------
1034 `__next__()`
1035 """
1036 return self.__next__()
1038 def rows(self):
1039 """The number of rows.
1041 Returns
1042 -------
1043 rows: int
1044 The number of rows contained in the table.
1045 """
1046 return max(map(len, self.data)) if self.data else 0
1048 def columns(self):
1049 """The number of columns.
1051 Returns
1052 -------
1053 columns: int
1054 The number of columns contained in the table.
1055 """
1056 return len(self.header)
1058 def row(self, index):
1059 """A single row of the table.
1061 Parameters
1062 ----------
1063 index: int
1064 The index of the row to be returned.
1066 Returns
1067 -------
1068 data: TableData
1069 A TableData object with a single row.
1070 """
1071 data = TableData()
1072 sec_indices = [-1] * self.nsecs
1073 for c in range(self.columns()):
1074 data.append(*self.column_head(c))
1075 for l in range(self.nsecs):
1076 s, i = self.section(c, l+1)
1077 if i != sec_indices[l]:
1078 data.header[-1].append(s)
1079 sec_indices[l] = i
1080 data.data[-1] = [self.data[c][index]]
1081 data.nsecs = self.nsecs
1082 return data
1084 def row_dict(self, index):
1085 """A single row of the table.
1087 Parameters
1088 ----------
1089 index: int
1090 The index of the row to be returned.
1092 Returns
1093 -------
1094 data: dict
1095 A dictionary with column header as key and corresponding data value of row `index`
1096 as value.
1097 """
1098 data = {}
1099 for c in range(self.columns()):
1100 data[self.label(c)] = self.data[c][index]
1101 return data
1103 def col(self, column):
1104 """A single column of the table.
1106 Parameters
1107 ----------
1108 column: None, int, or str
1109 The column to be returned.
1110 See self.index() for more information on how to specify a column.
1112 Returns
1113 -------
1114 table: TableData
1115 A TableData object with a single column.
1116 """
1117 data = TableData()
1118 c = self.index(column)
1119 data.append(*self.column_head(c))
1120 data.data = [self.data[c]]
1121 data.nsecs = 0
1122 return data
1124 def __call__(self, column):
1125 """A single column of the table as a ndarray.
1127 Parameters
1128 ----------
1129 column: None, int, or str
1130 The column to be returned.
1131 See self.index() for more information on how to specify a column.
1133 Returns
1134 -------
1135 data: 1-D ndarray
1136 Content of the specified column as a ndarray.
1137 """
1138 c = self.index(column)
1139 return np.asarray(self.data[c])
1141 def __setupkey(self, key):
1142 """Helper function that turns a key into row and column indices.
1144 Returns
1145 -------
1146 rows: list of int, slice, None
1147 Indices of selected rows.
1148 cols: list of int
1149 Indices of selected columns.
1151 Raises
1152 ------
1153 IndexError:
1154 If an invalid column was specified.
1155 """
1156 if type(key) is not tuple:
1157 rows = key
1158 cols = range(self.columns())
1159 else:
1160 rows = key[0]
1161 cols = key[1]
1162 if isinstance(cols, slice):
1163 start = cols.start
1164 if start is not None:
1165 start = self.index(start)
1166 if start is None:
1167 raise IndexError('"%s" is not a valid column index' % cols.start)
1168 stop = cols.stop
1169 if stop is not None:
1170 stop = self.index(stop)
1171 if stop is None:
1172 raise IndexError('"%s" is not a valid column index' % cols.stop)
1173 cols = slice(start, stop, cols.step)
1174 cols = range(self.columns())[cols]
1175 else:
1176 if not isinstance(cols, (list, tuple, np.ndarray)):
1177 cols = [cols]
1178 c = [self.index(inx) for inx in cols]
1179 if None in c:
1180 raise IndexError('"%s" is not a valid column index' % cols[c.index(None)])
1181 cols = c
1182 if isinstance(rows, np.ndarray) and rows.dtype == np.dtype(bool):
1183 rows = np.where(rows)[0]
1184 if len(rows) == 0:
1185 rows = None
1186 return rows, cols
1188 def __getitem__(self, key):
1189 """Data elements specified by slice.
1191 Parameters
1192 -----------
1193 key:
1194 First key specifies row, (optional) second one the column.
1195 Columns can be specified by index or name,
1196 see `index()` for details.
1198 Returns
1199 -------
1200 data:
1201 - A single data value if a single row and a single column is specified.
1202 - A ndarray of data elements if a single single column is specified.
1203 - A TableData object for multiple columns.
1204 - None if no row is selected (e.g. by a logical index that nowhere is True)
1206 Raises
1207 ------
1208 IndexError:
1209 If an invalid column was specified.
1210 """
1211 rows, cols = self.__setupkey(key)
1212 if len(cols) == 1:
1213 if rows is None:
1214 return None
1215 elif isinstance(rows, slice):
1216 return np.asarray(self.data[cols[0]][rows])
1217 elif isinstance(rows, (list, tuple, np.ndarray)):
1218 return np.asarray([self.data[cols[0]][r] for r in rows])
1219 else:
1220 return self.data[cols[0]][rows]
1221 else:
1222 data = TableData()
1223 sec_indices = [-1] * self.nsecs
1224 for c in cols:
1225 data.append(*self.column_head(c))
1226 for l in range(self.nsecs):
1227 s, i = self.section(c, l+1)
1228 if i != sec_indices[l]:
1229 data.header[-1].append(s)
1230 sec_indices[l] = i
1231 if rows is None:
1232 continue
1233 if isinstance(rows, (list, tuple, np.ndarray)):
1234 for r in rows:
1235 data.data[-1].append(self.data[c][r])
1236 else:
1237 if isinstance(self.data[c][rows], (list, tuple, np.ndarray)):
1238 data.data[-1].extend(self.data[c][rows])
1239 else:
1240 data.data[-1].append(self.data[c][rows])
1241 data.nsecs = self.nsecs
1242 return data
1244 def __setitem__(self, key, value):
1245 """Assign values to data elements specified by slice.
1247 Parameters
1248 -----------
1249 key:
1250 First key specifies row, (optional) second one the column.
1251 Columns can be specified by index or name,
1252 see `index()` for details.
1253 value: TableData, list, ndarray, float, ...
1254 Value(s) used to assing to the table elements as specified by `key`.
1256 Raises
1257 ------
1258 IndexError:
1259 If an invalid column was specified.
1260 """
1261 rows, cols = self.__setupkey(key)
1262 if rows is None:
1263 return
1264 if isinstance(value, TableData):
1265 if isinstance(self.data[cols[0]][rows], (list, tuple, np.ndarray)):
1266 for k, c in enumerate(cols):
1267 self.data[c][rows] = value.data[k]
1268 else:
1269 for k, c in enumerate(cols):
1270 self.data[c][rows] = value.data[k][0]
1271 else:
1272 if len(cols) == 1:
1273 if isinstance(rows, (list, tuple, np.ndarray)):
1274 if len(rows) == 1:
1275 self.data[cols[0]][rows[0]] = value
1276 elif isinstance(value, (list, tuple, np.ndarray)):
1277 for k, r in enumerate(rows):
1278 self.data[cols[0]][r] = value[k]
1279 else:
1280 for r in rows:
1281 self.data[cols[0]][r] = value
1282 elif isinstance(value, (list, tuple, np.ndarray)):
1283 self.data[cols[0]][rows] = value
1284 elif isinstance(rows, (np.integer, int)):
1285 self.data[cols[0]][rows] = value
1286 else:
1287 n = len(self.data[cols[0]][rows])
1288 if n > 1:
1289 self.data[cols[0]][rows] = [value]*n
1290 else:
1291 self.data[cols[0]][rows] = value
1292 else:
1293 if isinstance(self.data[0][rows], (list, tuple, np.ndarray)):
1294 for k, c in enumerate(cols):
1295 self.data[c][rows] = value[:,k]
1296 elif isinstance(value, (list, tuple, np.ndarray)):
1297 for k, c in enumerate(cols):
1298 self.data[c][rows] = value[k]
1299 else:
1300 for k, c in enumerate(cols):
1301 self.data[c][rows] = value
1303 def __delitem__(self, key):
1304 """Delete data elements or whole columns or rows.
1306 Parameters
1307 -----------
1308 key:
1309 First key specifies row, (optional) second one the column.
1310 Columns can be specified by index or name,
1311 see `index()` for details.
1312 If all rows are selected, then the specified columns are removed from the table.
1313 Otherwise only data values are removed.
1314 If all columns are selected than entire rows of data values are removed.
1315 Otherwise only data values in the specified rows are removed.
1317 Raises
1318 ------
1319 IndexError:
1320 If an invalid column was specified.
1321 """
1322 rows, cols = self.__setupkey(key)
1323 if rows is None:
1324 return
1325 row_indices = np.arange(self.rows(), dtype=int)[rows]
1326 if isinstance(row_indices, np.ndarray):
1327 if len(row_indices) == self.rows():
1328 # delete whole columns:
1329 self.remove(cols)
1330 elif len(row_indices) > 0:
1331 for r in reversed(sorted(row_indices)):
1332 for c in cols:
1333 del self.data[c][r]
1334 self.shape = (self.rows(), self.columns())
1335 else:
1336 for c in cols:
1337 del self.data[c][row_indices]
1338 self.shape = (self.rows(), self.columns())
1340 def array(self, row=None):
1341 """The table data as a ndarray.
1343 Parameters
1344 ----------
1345 row: int or None
1346 If specified, a 1D ndarray of that row will be returned.
1348 Returns
1349 -------
1350 data: 2D or 1D ndarray
1351 If no row is specified, the data content of the entire table
1352 as a 2D ndarray (rows first).
1353 If a row is specified, a 1D ndarray of that row.
1354 """
1355 if row is None:
1356 return np.array(self.data).T
1357 else:
1358 return np.array([d[row] for d in self.data])
1360 def data_frame(self):
1361 """The table data as a pandas DataFrame.
1363 Returns
1364 -------
1365 data: pandas.DataFrame
1366 A pandas DataFrame of the whole table.
1367 """
1368 return pd.DataFrame(self.dict())
1370 def dicts(self, raw_values=True, missing=default_missing_str):
1371 """The table as a list of dictionaries.
1373 Parameters
1374 ----------
1375 raw_values: bool
1376 If True, use raw table values as values,
1377 else format the values and add unit string.
1378 missing: str
1379 String indicating non-existing data elements.
1381 Returns
1382 -------
1383 table: list of dict
1384 For each row of the table a dictionary with header as key.
1385 """
1386 table = []
1387 for row in range(self.rows()):
1388 data = {}
1389 for col in range(len(self.header)):
1390 if raw_values:
1391 v = self.data[col][row];
1392 else:
1393 if isinstance(self.data[col][row], (float, np.floating)) and m.isnan(self.data[col][row]):
1394 v = missing
1395 else:
1396 u = ''
1397 if not self.units[col] in '1-' and self.units[col] != 'a.u.':
1398 u = self.units[col]
1399 v = (self.formats[col] % self.data[col][row]) + u
1400 data[self.header[col][0]] = v
1401 table.append(data)
1402 return table
1404 def dict(self):
1405 """The table as a dictionary.
1407 Returns
1408 -------
1409 table: dict
1410 A dictionary with keys being the column headers and
1411 values the list of data elements of the corresponding column.
1412 """
1413 table = {k: v for k, v in self.items()}
1414 return table
1416 def append_data(self, data, column=None):
1417 """Append data elements to successive columns.
1419 The current column is set behid the added columns.
1421 Parameters
1422 ----------
1423 data: float, int, str, etc. or list thereof or list of list thereof or dict or list of dict
1424 Data values to be appended to successive columns:
1425 - A single value is simply appened to the specified column of the table.
1426 - A 1D-list of values is appended to successive columns of the table
1427 starting with the specified column.
1428 - The columns of a 2D-list of values (second index) are appended
1429 to successive columns of the table starting with the specified column.
1430 - Values or list of values of a dictionary are added to the
1431 columns specified by the keys. Does not affect the current column.
1432 - The values of dictionaries in a list are appended to the
1433 columns specified by the keys. Does not affect the current column.
1434 column: None, int, or str
1435 The first column to which the data should be appended,
1436 if `data` does not specify columns.
1437 If None, append to the current column.
1438 See self.index() for more information on how to specify a column.
1439 """
1440 column = self.index(column)
1441 if column is None:
1442 column = self.setcol
1443 if isinstance(data, (list, tuple, np.ndarray)):
1444 if isinstance(data[0], (list, tuple, np.ndarray)):
1445 # 2D list, rows first:
1446 for row in data:
1447 for i, val in enumerate(row):
1448 self.data[column+i].append(val)
1449 self.setcol = column + len(data[0])
1450 elif isinstance(data[0], dict):
1451 # list of dictionaries:
1452 for row in data:
1453 for key in row:
1454 column = self.index(k)
1455 self.data[column].append(data[k])
1456 else:
1457 # 1D list:
1458 for val in data:
1459 self.data[column].append(val)
1460 column += 1
1461 self.setcol = column
1462 elif isinstance(data, dict):
1463 # dictionary with values:
1464 for key in data:
1465 column = self.index(key)
1466 if isinstance(data[key], (list, tuple, np.ndarray)):
1467 self.data[column].extend(data[key])
1468 else:
1469 self.data[column].append(data[key])
1470 else:
1471 # single value:
1472 self.data[column].append(data)
1473 self.setcol = column + 1
1474 if self.setcol >= len(self.data):
1475 self.setcol = 0
1476 self.shape = (self.rows(), self.columns())
1478 def append_data_column(self, data, column=None):
1479 """Append data elements to a column.
1481 The current column is incremented by one.
1483 Parameters
1484 ----------
1485 data: float, int, str, etc. or list thereof
1486 Data values to be appended to a column.
1487 column: None, int, or str
1488 The column to which the data should be appended.
1489 If None, append to the current column.
1490 See self.index() for more information on how to specify a column.
1491 """
1492 column = self.index(column)
1493 if column is None:
1494 column = self.setcol
1495 if isinstance(data, (list, tuple, np.ndarray)):
1496 self.data[column].extend(data)
1497 column += 1
1498 self.setcol = column
1499 else:
1500 self.data[column].append(data)
1501 self.setcol = column+1
1502 if self.setcol >= len(self.data):
1503 self.setcol = 0
1504 self.shape = (self.rows(), self.columns())
1506 def set_column(self, column):
1507 """Set the column where to add data.
1509 Parameters
1510 ----------
1511 column: int or str
1512 The column to which data elements should be appended.
1513 See self.index() for more information on how to specify a column.
1515 Raises
1516 ------
1517 IndexError:
1518 If an invalid column was specified.
1519 """
1520 col = self.index(column)
1521 if col is None:
1522 if isinstance(column, (np.integer, int)):
1523 column = '%d' % column
1524 raise IndexError('column ' + column + ' not found or invalid')
1525 self.setcol = col
1526 return col
1528 def fill_data(self):
1529 """Fill up all columns with missing data to have the same number of
1530 data elements.
1531 """
1532 # maximum rows:
1533 maxr = self.rows()
1534 # fill up:
1535 for c in range(len(self.data)):
1536 while len(self.data[c]) < maxr:
1537 self.data[c].append(np.nan)
1538 self.setcol = 0
1539 self.shape = (self.rows(), self.columns())
1541 def clear_data(self):
1542 """Clear content of the table but keep header.
1543 """
1544 for c in range(len(self.data)):
1545 self.data[c] = []
1546 self.setcol = 0
1547 self.shape = (self.rows(), self.columns())
1549 def sort(self, columns, reverse=False):
1550 """Sort the table rows in place.
1552 Parameters
1553 ----------
1554 columns: int or str or list of int or str
1555 A column specifier or a list of column specifiers of the columns
1556 to be sorted.
1557 reverse: boolean
1558 If `True` sort in descending order.
1560 Raises
1561 ------
1562 IndexError:
1563 If an invalid column was specified.
1564 """
1565 # fix columns:
1566 if not isinstance(columns, (list, tuple, np.ndarray)):
1567 columns = [ columns ]
1568 if not columns:
1569 return
1570 cols = []
1571 for col in columns:
1572 c = self.index(col)
1573 if c is None:
1574 if isinstance(col, (np.integer, int)):
1575 col = '%d' % col
1576 raise IndexError('sort column ' + col + ' not found')
1577 continue
1578 cols.append(c)
1579 # get sorted row indices:
1580 row_inx = range(self.rows())
1581 row_inx = sorted(row_inx, key=lambda x : [float('-inf') if self.data[c][x] is np.nan \
1582 or self.data[c][x] != self.data[c][x] \
1583 else self.data[c][x] for c in cols], reverse=reverse)
1584 # sort table according to indices:
1585 for c in range(self.columns()):
1586 self.data[c] = [self.data[c][r] for r in row_inx]
1588 def statistics(self):
1589 """Descriptive statistics of each column.
1590 """
1591 ds = TableData()
1592 if self.nsecs > 0:
1593 ds.append_section('statistics')
1594 for l in range(1,self.nsecs):
1595 ds.append_section('-')
1596 ds.append('-', '-', '%-10s')
1597 else:
1598 ds.append('statistics', '-', '%-10s')
1599 ds.append_data('mean', 0)
1600 ds.append_data('std', 0)
1601 ds.append_data('min', 0)
1602 ds.append_data('quartile1', 0)
1603 ds.append_data('median', 0)
1604 ds.append_data('quartile3', 0)
1605 ds.append_data('max', 0)
1606 ds.append_data('count', 0)
1607 dc = 1
1608 for c in range(self.columns()):
1609 if len(self.data[c]) > 0 and isinstance(self.data[c][0], (float, int, np.floating, np.integer)):
1610 ds.hidden.append(False)
1611 ds.header.append(self.header[c])
1612 ds.units.append(self.units[c])
1613 # integer data still make floating point statistics:
1614 if isinstance(self.data[c][0], (float, np.floating)):
1615 f = self.formats[c]
1616 i0 = f.find('.')
1617 if i0 > 0:
1618 p = int(f[i0+1:-1])
1619 if p <= 0:
1620 f = '%.1f'
1621 ds.formats.append(f)
1622 else:
1623 ds.formats.append('%.1f')
1624 # remove nans:
1625 data = np.asarray(self.data[c], float)
1626 data = data[np.isfinite(data)]
1627 # compute statistics:
1628 ds.data.append([])
1629 ds.append_data(np.mean(data), dc)
1630 ds.append_data(np.std(data), dc)
1631 ds.append_data(np.min(data), dc)
1632 q1, m, q3 = np.percentile(data, [25., 50., 75.])
1633 ds.append_data(q1, dc)
1634 ds.append_data(m, dc)
1635 ds.append_data(q3, dc)
1636 ds.append_data(np.max(data), dc)
1637 ds.append_data(len(data), dc)
1638 dc += 1
1639 ds.nsecs = self.nsecs
1640 ds.shape = (ds.rows(), ds.columns())
1641 return ds
1643 def key_value(self, row, col, missing=default_missing_str):
1644 """A data element returned as a key-value pair.
1646 Parameters
1647 ----------
1648 row: int
1649 Specifies the row from which the data element should be retrieved.
1650 col: None, int, or str
1651 A specification of a column.
1652 See self.index() for more information on how to specify a column.
1653 missing: str
1654 String indicating non-existing data elements.
1656 Returns
1657 -------
1658 key: str
1659 Header label of the column
1660 value: str
1661 A textual representation of the data element according to the format
1662 of the column, followed by the unit of the column.
1663 """
1664 col = self.index(col)
1665 if col is None:
1666 return ''
1667 if isinstance(self.data[col][row], (float, np.floating)) and m.isnan(self.data[col][row]):
1668 v = missing
1669 else:
1670 u = ''
1671 if not self.units[col] in '1-' and self.units[col] != 'a.u.':
1672 u = self.units[col]
1673 v = (self.formats[col] % self.data[col][row]) + u
1674 return self.header[col][0], v
1676 def hide(self, column):
1677 """Hide a column or a range of columns.
1679 Hidden columns will not be printed out by the write() function.
1681 Parameters
1682 ----------
1683 column: int or str
1684 The column to be hidden.
1685 See self.index() for more information on how to specify a column.
1686 """
1687 c0, c1 = self.find_col(column)
1688 if c0 is not None:
1689 for c in range(c0, c1):
1690 self.hidden[c] = True
1692 def hide_all(self):
1693 """Hide all columns.
1695 Hidden columns will not be printed out by the write() function.
1696 """
1697 for c in range(len(self.hidden)):
1698 self.hidden[c] = True
1700 def hide_empty_columns(self, missing=default_missing_inputs):
1701 """Hide all columns that do not contain data.
1703 Hidden columns will not be printed out by the write() function.
1705 Parameters
1706 ----------
1707 missing: list of str
1708 Strings indicating missing data.
1709 """
1710 for c in range(len(self.data)):
1711 # check for empty column:
1712 isempty = True
1713 for v in self.data[c]:
1714 if isinstance(v, (float, np.floating)):
1715 if not m.isnan(v):
1716 isempty = False
1717 break
1718 else:
1719 if not v in missing:
1720 isempty = False
1721 break
1722 if isempty:
1723 self.hidden[c] = True
1725 def show(self, column):
1726 """Show a column or a range of columns.
1728 Undoes hiding of a column.
1730 Parameters
1731 ----------
1732 column: int or str
1733 The column to be shown.
1734 See self.index() for more information on how to specify a column.
1735 """
1736 c0, c1 = self.find_col(column)
1737 if c0 is not None:
1738 for c in range(c0, c1):
1739 self.hidden[c] = False
1741 def write(self, fh=sys.stdout, table_format=None, delimiter=None,
1742 unit_style=None, column_numbers=None, sections=None,
1743 align_columns=None, shrink_width=True,
1744 missing=default_missing_str, center_columns=False,
1745 latex_label_command='', latex_merge_std=False):
1746 """Write the table to a file or stream.
1748 Parameters
1749 ----------
1750 fh: filename or stream
1751 If not a stream, the file with name `fh` is opened.
1752 If `fh` does not have an extension,
1753 the `table_format` is appended as an extension.
1754 Otherwise `fh` is used as a stream for writing.
1755 table_format: None or str
1756 The format to be used for output.
1757 One of 'out', 'dat', 'ascii', 'csv', 'rtai', 'md', 'tex', 'html'.
1758 If None or 'auto' then the format is set to the extension of the filename given by `fh`.
1759 If `fh` is a stream the format is set to 'dat'.
1760 delimiter: str
1761 String or character separating columns, if supported by the `table_format`.
1762 If None or 'auto' use the default for the specified `table_format`.
1763 unit_style: None or str
1764 - None or 'auto': use default of the specified `table_format`.
1765 - 'row': write an extra row to the table header specifying the units of the columns.
1766 - 'header': add the units to the column headers.
1767 - 'none': do not specify the units.
1768 column_numbers: str or None
1769 Add a row specifying the column index:
1770 - 'index': indices are integers, first column is 0.
1771 - 'num': indices are integers, first column is 1.
1772 - 'aa': use 'a', 'b', 'c', ..., 'z', 'aa', 'ab', ... for indexing
1773 - 'aa': use 'A', 'B', 'C', ..., 'Z', 'AA', 'AB', ... for indexing
1774 - None or 'none': do not add a row with column indices
1775 TableData.column_numbering is a list with the supported styles.
1776 sections: None or int
1777 Number of section levels to be printed.
1778 If `None` or 'auto' use default of selected `table_format`.
1779 align_columns: boolean
1780 - `True`: set width of column formats to make them align.
1781 - `False`: set width of column formats to 0 - no unnecessary spaces.
1782 - None or 'auto': Use default of the selected `table_format`.
1783 shrink_width: boolean
1784 If `True` disregard width specified by the format strings,
1785 such that columns can become narrower.
1786 missing: str
1787 Indicate missing data by this string.
1788 center_columns: boolean
1789 If True center all columns (markdown, html, and latex).
1790 latex_label_command: str
1791 LaTeX command for formatting header labels.
1792 E.g. 'textbf' for making the header labels bold.
1793 latex_merge_std: str
1794 Merge header of columns with standard deviations with previous column
1795 (LaTeX tables only).
1797 Returns
1798 -------
1799 file_name: str or None
1800 The full name of the file into which the data were written.
1802 Supported file formats
1803 ----------------------
1805 ## `dat`: data text file
1806 ``` plain
1807 # info reaction
1808 # size weight delay jitter
1809 # m kg ms mm
1810 2.34 123 98.7 23
1811 56.70 3457 54.3 45
1812 8.90 43 67.9 345
1813 ```
1815 ## `ascii`: ascii-art table
1816 ``` plain
1817 |---------------------------------|
1818 | info | reaction |
1819 | size | weight | delay | jitter |
1820 | m | kg | ms | mm |
1821 |-------|--------|-------|--------|
1822 | 2.34 | 123 | 98.7 | 23 |
1823 | 56.70 | 3457 | 54.3 | 45 |
1824 | 8.90 | 43 | 67.9 | 345 |
1825 |---------------------------------|
1826 ```
1828 ## `csv`: comma separated values
1829 ``` plain
1830 size/m,weight/kg,delay/ms,jitter/mm
1831 2.34,123,98.7,23
1832 56.70,3457,54.3,45
1833 8.90,43,67.9,345
1834 ```
1836 ## `rtai`: rtai-style table
1837 ``` plain
1838 RTH| info | reaction
1839 RTH| size | weight| delay| jitter
1840 RTH| m | kg | ms | mm
1841 RTD| 2.34| 123| 98.7| 23
1842 RTD| 56.70| 3457| 54.3| 45
1843 RTD| 8.90| 43| 67.9| 345
1844 ```
1846 ## `md`: markdown
1847 ``` plain
1848 | size/m | weight/kg | delay/ms | jitter/mm |
1849 |------:|-------:|------:|-------:|
1850 | 2.34 | 123 | 98.7 | 23 |
1851 | 56.70 | 3457 | 54.3 | 45 |
1852 | 8.90 | 43 | 67.9 | 345 |
1853 ```
1855 ## `tex`: latex tabular
1856 ``` tex
1857 \\begin{tabular}{rrrr}
1858 \\hline
1859 \\multicolumn{2}{l}{info} & \\multicolumn{2}{l}{reaction} \\
1860 \\multicolumn{1}{l}{size} & \\multicolumn{1}{l}{weight} & \\multicolumn{1}{l}{delay} & \\multicolumn{1}{l}{jitter} \\
1861 \\multicolumn{1}{l}{m} & \\multicolumn{1}{l}{kg} & \\multicolumn{1}{l}{ms} & \\multicolumn{1}{l}{mm} \\
1862 \\hline
1863 2.34 & 123 & 98.7 & 23 \\
1864 56.70 & 3457 & 54.3 & 45 \\
1865 8.90 & 43 & 67.9 & 345 \\
1866 \\hline
1867 \\end{tabular}
1868 ```
1870 ## `html`: html
1871 ``` html
1872 <table>
1873 <thead>
1874 <tr class="header">
1875 <th align="left" colspan="2">info</th>
1876 <th align="left" colspan="2">reaction</th>
1877 </tr>
1878 <tr class="header">
1879 <th align="left">size</th>
1880 <th align="left">weight</th>
1881 <th align="left">delay</th>
1882 <th align="left">jitter</th>
1883 </tr>
1884 <tr class="header">
1885 <th align="left">m</th>
1886 <th align="left">kg</th>
1887 <th align="left">ms</th>
1888 <th align="left">mm</th>
1889 </tr>
1890 </thead>
1891 <tbody>
1892 <tr class"odd">
1893 <td align="right">2.34</td>
1894 <td align="right">123</td>
1895 <td align="right">98.7</td>
1896 <td align="right">23</td>
1897 </tr>
1898 <tr class"even">
1899 <td align="right">56.70</td>
1900 <td align="right">3457</td>
1901 <td align="right">54.3</td>
1902 <td align="right">45</td>
1903 </tr>
1904 <tr class"odd">
1905 <td align="right">8.90</td>
1906 <td align="right">43</td>
1907 <td align="right">67.9</td>
1908 <td align="right">345</td>
1909 </tr>
1910 </tbody>
1911 </table>
1912 ```
1913 """
1914 # fix parameter:
1915 if table_format == 'auto':
1916 table_format = None
1917 if delimiter == 'auto':
1918 delimiter = None
1919 if unit_style == 'auto':
1920 unit_style = None
1921 if column_numbers == 'none':
1922 column_numbers = None
1923 if sections == 'auto':
1924 sections = None
1925 if align_columns == 'auto':
1926 align_columns = None
1927 # open file:
1928 own_file = False
1929 file_name = None
1930 if not hasattr(fh, 'write'):
1931 _, ext = os.path.splitext(fh)
1932 if table_format is None:
1933 if len(ext) > 1 and ext[1:] in self.ext_formats:
1934 table_format = self.ext_formats[ext[1:]]
1935 elif not ext or not ext[1:].lower() in self.ext_formats:
1936 fh += '.' + self.extensions[table_format]
1937 file_name = fh
1938 fh = open(fh, 'w')
1939 own_file = True
1940 if table_format is None:
1941 table_format = 'dat'
1942 # set style:
1943 if table_format[0] == 'd':
1944 align_columns = True
1945 begin_str = ''
1946 end_str = ''
1947 header_start = '# '
1948 header_sep = ' '
1949 header_close = ''
1950 header_end = '\n'
1951 data_start = ' '
1952 data_sep = ' '
1953 data_close = ''
1954 data_end = '\n'
1955 top_line = False
1956 header_line = False
1957 bottom_line = False
1958 if delimiter is not None:
1959 header_sep = delimiter
1960 data_sep = delimiter
1961 if sections is None:
1962 sections = 1000
1963 elif table_format[0] == 'a':
1964 align_columns = True
1965 begin_str = ''
1966 end_str = ''
1967 header_start = '| '
1968 header_sep = ' | '
1969 header_close = ''
1970 header_end = ' |\n'
1971 data_start = '| '
1972 data_sep = ' | '
1973 data_close = ''
1974 data_end = ' |\n'
1975 top_line = True
1976 header_line = True
1977 bottom_line = True
1978 if delimiter is not None:
1979 header_sep = delimiter
1980 data_sep = delimiter
1981 if sections is None:
1982 sections = 1000
1983 elif table_format[0] == 'c':
1984 # csv according to http://www.ietf.org/rfc/rfc4180.txt :
1985 column_numbers=None
1986 if unit_style is None:
1987 unit_style = 'header'
1988 if align_columns is None:
1989 align_columns = False
1990 begin_str = ''
1991 end_str = ''
1992 header_start=''
1993 header_sep = ','
1994 header_close = ''
1995 header_end='\n'
1996 data_start=''
1997 data_sep = ','
1998 data_close = ''
1999 data_end='\n'
2000 top_line = False
2001 header_line = False
2002 bottom_line = False
2003 if delimiter is not None:
2004 header_sep = delimiter
2005 data_sep = delimiter
2006 if sections is None:
2007 sections = 0
2008 elif table_format[0] == 'r':
2009 align_columns = True
2010 begin_str = ''
2011 end_str = ''
2012 header_start = 'RTH| '
2013 header_sep = '| '
2014 header_close = ''
2015 header_end = '\n'
2016 data_start = 'RTD| '
2017 data_sep = '| '
2018 data_close = ''
2019 data_end = '\n'
2020 top_line = False
2021 header_line = False
2022 bottom_line = False
2023 if sections is None:
2024 sections = 1000
2025 elif table_format[0] == 'm':
2026 if unit_style is None or unit_style == 'row':
2027 unit_style = 'header'
2028 align_columns = True
2029 begin_str = ''
2030 end_str = ''
2031 header_start='| '
2032 header_sep = ' | '
2033 header_close = ''
2034 header_end=' |\n'
2035 data_start='| '
2036 data_sep = ' | '
2037 data_close = ''
2038 data_end=' |\n'
2039 top_line = False
2040 header_line = True
2041 bottom_line = False
2042 if sections is None:
2043 sections = 0
2044 elif table_format[0] == 'h':
2045 align_columns = False
2046 begin_str = '<table>\n<thead>\n'
2047 end_str = '</tbody>\n</table>\n'
2048 if center_columns:
2049 header_start=' <tr>\n <th align="center"'
2050 header_sep = '</th>\n <th align="center"'
2051 else:
2052 header_start=' <tr>\n <th align="left"'
2053 header_sep = '</th>\n <th align="left"'
2054 header_close = '>'
2055 header_end='</th>\n </tr>\n'
2056 data_start=' <tr>\n <td'
2057 data_sep = '</td>\n <td'
2058 data_close = '>'
2059 data_end='</td>\n </tr>\n'
2060 top_line = False
2061 header_line = False
2062 bottom_line = False
2063 if sections is None:
2064 sections = 1000
2065 elif table_format[0] == 't':
2066 if align_columns is None:
2067 align_columns = False
2068 begin_str = '\\begin{tabular}'
2069 end_str = '\\end{tabular}\n'
2070 header_start=' '
2071 header_sep = ' & '
2072 header_close = ''
2073 header_end=' \\\\\n'
2074 data_start=' '
2075 data_sep = ' & '
2076 data_close = ''
2077 data_end=' \\\\\n'
2078 top_line = True
2079 header_line = True
2080 bottom_line = True
2081 if sections is None:
2082 sections = 1000
2083 else:
2084 if align_columns is None:
2085 align_columns = True
2086 begin_str = ''
2087 end_str = ''
2088 header_start = ''
2089 header_sep = ' '
2090 header_close = ''
2091 header_end = '\n'
2092 data_start = ''
2093 data_sep = ' '
2094 data_close = ''
2095 data_end = '\n'
2096 top_line = False
2097 header_line = False
2098 bottom_line = False
2099 if sections is None:
2100 sections = 1000
2101 # check units:
2102 if unit_style is None:
2103 unit_style = 'row'
2104 have_units = False
2105 for u in self.units:
2106 if u and u != '1' and u != '-':
2107 have_units = True
2108 break
2109 if not have_units:
2110 unit_style = 'none'
2111 # find std columns:
2112 stdev_col = np.zeros(len(self.header), dtype=bool)
2113 for c in range(len(self.header)-1):
2114 if self.header[c+1][0].lower() in ['sd', 'std', 's.d.', 'stdev'] and \
2115 not self.hidden[c+1]:
2116 stdev_col[c] = True
2117 # begin table:
2118 fh.write(begin_str)
2119 if table_format[0] == 't':
2120 fh.write('{')
2121 merged = False
2122 for h, f, s in zip(self.hidden, self.formats, stdev_col):
2123 if merged:
2124 fh.write('l')
2125 merged = False
2126 continue
2127 if h:
2128 continue
2129 if latex_merge_std and s:
2130 fh.write('r@{$\\,\\pm\\,$}')
2131 merged = True
2132 elif center_columns:
2133 fh.write('c')
2134 elif f[1] == '-':
2135 fh.write('l')
2136 else:
2137 fh.write('r')
2138 fh.write('}\n')
2139 # retrieve column formats and widths:
2140 widths = []
2141 widths_pos = []
2142 for c, f in enumerate(self.formats):
2143 w = 0
2144 # position of width specification:
2145 i0 = 1
2146 if len(f) > 1 and f[1] == '-' :
2147 i0 = 2
2148 i1 = f.find('.')
2149 if not shrink_width:
2150 if f[i0:i1]:
2151 w = int(f[i0:i1])
2152 widths_pos.append((i0, i1))
2153 # adapt width to header label:
2154 hw = len(self.header[c][0])
2155 if unit_style == 'header' and self.units[c] and\
2156 self.units[c] != '1' and self.units[c] != '-':
2157 hw += 1 + len(self.units[c])
2158 if w < hw:
2159 w = hw
2160 # adapt width to data:
2161 if f[-1] == 's':
2162 for v in self.data[c]:
2163 if not isinstance(v, (float, np.floating)) and w < len(v):
2164 w = len(v)
2165 else:
2166 fs = f[:i0] + str(0) + f[i1:]
2167 for v in self.data[c]:
2168 if isinstance(v, (float, np.floating)) and m.isnan(v):
2169 s = missing
2170 else:
2171 try:
2172 s = fs % v
2173 except ValueError:
2174 s = missing
2175 if w < len(s):
2176 w = len(s)
2177 widths.append(w)
2178 # adapt width to sections:
2179 sec_indices = [0] * self.nsecs
2180 sec_widths = [0] * self.nsecs
2181 sec_columns = [0] * self.nsecs
2182 for c in range(len(self.header)):
2183 w = widths[c]
2184 for l in range(min(self.nsecs, sections)):
2185 if 1+l < len(self.header[c]):
2186 if c > 0 and sec_columns[l] > 0 and \
2187 1+l < len(self.header[sec_indices[l]]) and \
2188 len(self.header[sec_indices[l]][1+l]) > sec_widths[l]:
2189 dw = len(self.header[sec_indices[l]][1+l]) - sec_widths[l]
2190 nc = sec_columns[l]
2191 ddw = np.zeros(nc, dtype=int) + dw // nc
2192 ddw[:dw % nc] += 1
2193 wk = 0
2194 for ck in range(sec_indices[l], c):
2195 if not self.hidden[ck]:
2196 widths[ck] += ddw[wk]
2197 wk += 1
2198 sec_widths[l] = 0
2199 sec_indices[l] = c
2200 if not self.hidden[c]:
2201 if sec_widths[l] > 0:
2202 sec_widths[l] += len(header_sep)
2203 sec_widths[l] += w
2204 sec_columns[l] += 1
2205 # set width of format string:
2206 formats = []
2207 for c, (f, w) in enumerate(zip(self.formats, widths)):
2208 formats.append(f[:widths_pos[c][0]] + str(w) + f[widths_pos[c][1]:])
2209 # top line:
2210 if top_line:
2211 if table_format[0] == 't':
2212 fh.write(' \\hline \\\\[-2ex]\n')
2213 else:
2214 first = True
2215 fh.write(header_start.replace(' ', '-'))
2216 for c in range(len(self.header)):
2217 if self.hidden[c]:
2218 continue
2219 if not first:
2220 fh.write('-'*len(header_sep))
2221 first = False
2222 fh.write(header_close)
2223 w = widths[c]
2224 fh.write(w*'-')
2225 fh.write(header_end.replace(' ', '-'))
2226 # section and column headers:
2227 nsec0 = self.nsecs-sections
2228 if nsec0 < 0:
2229 nsec0 = 0
2230 for ns in range(nsec0, self.nsecs+1):
2231 nsec = self.nsecs-ns
2232 first = True
2233 last = False
2234 merged = False
2235 fh.write(header_start)
2236 for c in range(len(self.header)):
2237 if nsec < len(self.header[c]):
2238 # section width and column count:
2239 sw = -len(header_sep)
2240 columns = 0
2241 if not self.hidden[c]:
2242 sw = widths[c]
2243 columns = 1
2244 for k in range(c+1, len(self.header)):
2245 if nsec < len(self.header[k]):
2246 break
2247 if self.hidden[k]:
2248 continue
2249 sw += len(header_sep) + widths[k]
2250 columns += 1
2251 else:
2252 last = True
2253 if len(header_end.strip()) == 0:
2254 sw = 0 # last entry needs no width
2255 if columns == 0:
2256 continue
2257 if not first and not merged:
2258 fh.write(header_sep)
2259 first = False
2260 if table_format[0] == 'c':
2261 sw -= len(header_sep)*(columns-1)
2262 elif table_format[0] == 'h':
2263 if columns>1:
2264 fh.write(' colspan="%d"' % columns)
2265 elif table_format[0] == 't':
2266 if merged:
2267 merged = False
2268 continue
2269 if latex_merge_std and nsec == 0 and stdev_col[c]:
2270 merged = True
2271 fh.write('\\multicolumn{%d}{c}{' % (columns+1))
2272 elif center_columns:
2273 fh.write('\\multicolumn{%d}{c}{' % columns)
2274 else:
2275 fh.write('\\multicolumn{%d}{l}{' % columns)
2276 if latex_label_command:
2277 fh.write('\\%s{' % latex_label_command)
2278 fh.write(header_close)
2279 hs = self.header[c][nsec]
2280 if nsec == 0 and unit_style == 'header':
2281 if self.units[c] and self.units[c] != '1' and self.units[c] != '-':
2282 hs += '/' + self.units[c]
2283 if align_columns and not table_format[0] in 'th':
2284 f = '%%-%ds' % sw
2285 fh.write(f % hs)
2286 else:
2287 fh.write(hs)
2288 if table_format[0] == 'c':
2289 if not last:
2290 fh.write(header_sep*(columns-1))
2291 elif table_format[0] == 't':
2292 if latex_label_command:
2293 fh.write('}')
2294 fh.write('}')
2295 fh.write(header_end)
2296 # units:
2297 if unit_style == 'row':
2298 first = True
2299 merged = False
2300 fh.write(header_start)
2301 for c in range(len(self.header)):
2302 if self.hidden[c] or merged:
2303 merged = False
2304 continue
2305 if not first:
2306 fh.write(header_sep)
2307 first = False
2308 fh.write(header_close)
2309 unit = self.units[c]
2310 if not unit:
2311 unit = '-'
2312 if table_format[0] == 't':
2313 if latex_merge_std and stdev_col[c]:
2314 merged = True
2315 fh.write('\\multicolumn{2}{c}{%s}' % latex_unit(unit))
2316 elif center_columns:
2317 fh.write('\\multicolumn{1}{c}{%s}' % latex_unit(unit))
2318 else:
2319 fh.write('\\multicolumn{1}{l}{%s}' % latex_unit(unit))
2320 else:
2321 if align_columns and not table_format[0] in 'h':
2322 f = '%%-%ds' % widths[c]
2323 fh.write(f % unit)
2324 else:
2325 fh.write(unit)
2326 fh.write(header_end)
2327 # column numbers:
2328 if column_numbers is not None:
2329 first = True
2330 fh.write(header_start)
2331 for c in range(len(self.header)):
2332 if self.hidden[c]:
2333 continue
2334 if not first:
2335 fh.write(header_sep)
2336 first = False
2337 fh.write(header_close)
2338 i = c
2339 if column_numbers == 'num':
2340 i = c+1
2341 aa = index2aa(c, 'a')
2342 if column_numbers == 'AA':
2343 aa = index2aa(c, 'A')
2344 if table_format[0] == 't':
2345 if column_numbers == 'num' or column_numbers == 'index':
2346 fh.write('\\multicolumn{1}{l}{%d}' % i)
2347 else:
2348 fh.write('\\multicolumn{1}{l}{%s}' % aa)
2349 else:
2350 if column_numbers == 'num' or column_numbers == 'index':
2351 if align_columns:
2352 f = '%%%dd' % widths[c]
2353 fh.write(f % i)
2354 else:
2355 fh.write('%d' % i)
2356 else:
2357 if align_columns:
2358 f = '%%-%ds' % widths[c]
2359 fh.write(f % aa)
2360 else:
2361 fh.write(aa)
2362 fh.write(header_end)
2363 # header line:
2364 if header_line:
2365 if table_format[0] == 'm':
2366 fh.write('|')
2367 for c in range(len(self.header)):
2368 if self.hidden[c]:
2369 continue
2370 w = widths[c]+2
2371 if center_columns:
2372 fh.write(':' + (w-2)*'-' + ':|')
2373 elif formats[c][1] == '-':
2374 fh.write(w*'-' + '|')
2375 else:
2376 fh.write((w-1)*'-' + ':|')
2377 fh.write('\n')
2378 elif table_format[0] == 't':
2379 fh.write(' \\hline \\\\[-2ex]\n')
2380 else:
2381 first = True
2382 fh.write(header_start.replace(' ', '-'))
2383 for c in range(len(self.header)):
2384 if self.hidden[c]:
2385 continue
2386 if not first:
2387 fh.write(header_sep.replace(' ', '-'))
2388 first = False
2389 fh.write(header_close)
2390 w = widths[c]
2391 fh.write(w*'-')
2392 fh.write(header_end.replace(' ', '-'))
2393 # start table data:
2394 if table_format[0] == 'h':
2395 fh.write('</thead>\n<tbody>\n')
2396 # data:
2397 for k in range(self.rows()):
2398 first = True
2399 merged = False
2400 fh.write(data_start)
2401 for c, f in enumerate(formats):
2402 if self.hidden[c] or merged:
2403 merged = False
2404 continue
2405 if not first:
2406 fh.write(data_sep)
2407 first = False
2408 if table_format[0] == 'h':
2409 if center_columns:
2410 fh.write(' align="center"')
2411 elif f[1] == '-':
2412 fh.write(' align="left"')
2413 else:
2414 fh.write(' align="right"')
2415 fh.write(data_close)
2416 if k >= len(self.data[c]) or \
2417 (isinstance(self.data[c][k], (float, np.floating)) and m.isnan(self.data[c][k])):
2418 # missing data:
2419 if table_format[0] == 't' and latex_merge_std and stdev_col[c]:
2420 merged = True
2421 fh.write('\\multicolumn{2}{c}{%s}' % missing)
2422 elif align_columns:
2423 if f[1] == '-':
2424 fn = '%%-%ds' % widths[c]
2425 else:
2426 fn = '%%%ds' % widths[c]
2427 fh.write(fn % missing)
2428 else:
2429 fh.write(missing)
2430 else:
2431 # data value:
2432 try:
2433 ds = f % self.data[c][k]
2434 except ValueError:
2435 ds = missing
2436 if not align_columns:
2437 ds = ds.strip()
2438 fh.write(ds)
2439 fh.write(data_end)
2440 # bottom line:
2441 if bottom_line:
2442 if table_format[0] == 't':
2443 fh.write(' \\hline\n')
2444 else:
2445 first = True
2446 fh.write(header_start.replace(' ', '-'))
2447 for c in range(len(self.header)):
2448 if self.hidden[c]:
2449 continue
2450 if not first:
2451 fh.write('-'*len(header_sep))
2452 first = False
2453 fh.write(header_close)
2454 w = widths[c]
2455 fh.write(w*'-')
2456 fh.write(header_end.replace(' ', '-'))
2457 # end table:
2458 fh.write(end_str)
2459 # close file:
2460 if own_file:
2461 fh.close()
2462 # return file name:
2463 return file_name
2466 def write_file_stream(self, basename, file_name, **kwargs):
2467 """Write table to file or stream and return appropriate file name.
2469 Parameters
2470 ----------
2471 basename: str or stream
2472 If str, path and basename of file.
2473 `file_name` and an extension are appended.
2474 If stream, write table data into this stream.
2475 file_name: str
2476 Name of file that is appended to a base path or `basename`.
2477 kwargs:
2478 Arguments passed on to `TableData.write()`.
2479 In particular, 'table_format' is used to the set the file extension
2480 that is appended to the returned `file_name`.
2482 Returns
2483 -------
2484 file_name: str
2485 Path and full name of the written file in case of `basename`
2486 being a string. Otherwise, the file name and extension that
2487 should be appended to a base path.
2488 """
2489 if hasattr(basename, 'write'):
2490 table_format = kwargs.get('table_format', None)
2491 if table_format is None or table_format == 'auto':
2492 table_format = 'csv'
2493 file_name += '.' + TableData.extensions[table_format]
2494 self.write(basename, **kwargs)
2495 return file_name
2496 else:
2497 file_name = self.write(basename + file_name, **kwargs)
2498 return file_name
2501 def __str__(self):
2502 """Write table to a string.
2503 """
2504 stream = StringIO()
2505 self.write(stream, table_format='out')
2506 return stream.getvalue()
2509 def load(self, fh, missing=default_missing_inputs, stop=None):
2510 """Load table from file or stream.
2512 File type and properties are automatically inferred.
2514 Parameters
2515 ----------
2516 fh: str or stream
2517 If not a stream, the file with name `fh` is opened for reading.
2518 missing: str or list of str
2519 Missing data are indicated by this string and
2520 are translated to np.nan.
2521 stop: str or None
2522 If the beginning of a line matches `stop`, then stop reading the file.
2524 Raises
2525 ------
2526 FileNotFoundError:
2527 If `fh` is a path that does not exist.
2528 """
2530 def read_key_line(line, sep, table_format):
2531 if sep is None:
2532 cols, indices = zip(*[(m.group(0), m.start()) for m in re.finditer(r'( ?[\S]+)+(?=[ ][ ]+|\Z)', line.strip())])
2533 elif table_format == 'csv':
2534 cols, indices = zip(*[(c.strip(), i) for i, c in enumerate(line.strip().split(sep)) if c.strip()])
2535 return cols, indices
2536 else:
2537 seps = r'[^'+re.escape(sep)+']+'
2538 cols, indices = zip(*[(m.group(0), m.start()) for m in re.finditer(seps, line.strip())])
2539 colss = []
2540 indicess = []
2541 if table_format == 'tex':
2542 i = 0
2543 for c in cols:
2544 if 'multicolumn' in c:
2545 fields = c.split('{')
2546 n = int(fields[1].strip().rstrip('}').rstrip())
2547 colss.append(fields[3].strip().rstrip('}').rstrip())
2548 indicess.append(i)
2549 i += n
2550 else:
2551 colss.append(c.strip())
2552 indicess.append(i)
2553 i += 1
2554 else:
2555 for k, (c, i) in enumerate(zip(cols, indices)):
2556 if k == 0:
2557 c = c.lstrip('|')
2558 if k == len(cols)-1:
2559 c = c.rstrip('|')
2560 cs = c.strip()
2561 colss.append(cs)
2562 indicess.append(i)
2563 return colss, indicess
2565 def read_data_line(line, sep, post, precd, alld, numc, exped,
2566 fixed, strf, missing, nans):
2567 # read line:
2568 cols = []
2569 if sep is None:
2570 cols = [m.group(0) for m in re.finditer(r'\S+', line.strip())]
2571 else:
2572 if sep.isspace():
2573 seps = r'[^'+re.escape(sep)+']+'
2574 cols = [m.group(0) for m in re.finditer(seps, line.strip())]
2575 else:
2576 cols = line.split(sep)
2577 if len(cols) > 0 and len(cols[0]) == 0:
2578 cols = cols[1:]
2579 if len(cols) > 0 and len(cols[-1]) == 0:
2580 cols = cols[:-1]
2581 if len(cols) > 0:
2582 cols[0] = cols[0].lstrip('|').lstrip()
2583 cols[-1] = cols[-1].rstrip('|').rstrip()
2584 cols = [c.strip() for c in cols if c != '|']
2585 # read columns:
2586 for k, c in enumerate(cols):
2587 try:
2588 v = float(c)
2589 ad = 0
2590 ve = c.split('e')
2591 if len(ve) <= 1:
2592 exped[k] = False
2593 else:
2594 ad = len(ve[1])+1
2595 vc = ve[0].split('.')
2596 ad += len(vc[0])
2597 prec = len(vc[0].lstrip('-').lstrip('+').lstrip('0'))
2598 if len(vc) == 2:
2599 if numc[k] and post[k] != len(vc[1]):
2600 fixed[k] = False
2601 if post[k] < len(vc[1]):
2602 post[k] = len(vc[1])
2603 ad += len(vc[1])+1
2604 prec += len(vc[1].rstrip('0'))
2605 if precd[k] < prec:
2606 precd[k] = prec
2607 if alld[k] < ad:
2608 alld[k] = ad
2609 numc[k] = True
2610 except ValueError:
2611 if c in missing:
2612 v = np.nan
2613 nans[k] = c
2614 else:
2615 strf[k] = True
2616 if alld[k] < len(c):
2617 alld[k] = len(c)
2618 v = c
2619 self.append_data(v, k)
2621 # initialize:
2622 if isinstance(missing, str):
2623 missing = [missing]
2624 self.data = []
2625 self.shape = (0, 0)
2626 self.header = []
2627 self.nsecs = 0
2628 self.units = []
2629 self.formats = []
2630 self.hidden = []
2631 self.setcol = 0
2632 self.addcol = 0
2633 # open file:
2634 own_file = False
2635 if not hasattr(fh, 'readline'):
2636 fh = open(fh, 'r')
2637 own_file = True
2638 # read inital lines of file:
2639 key = []
2640 data = []
2641 target = data
2642 comment = False
2643 table_format='dat'
2644 for line in fh:
2645 line = line.rstrip()
2646 if line == stop:
2647 break;
2648 if line:
2649 if r'\begin{tabular' in line:
2650 table_format='tex'
2651 target = key
2652 continue
2653 if table_format == 'tex':
2654 if r'\end{tabular' in line:
2655 break
2656 if r'\hline' in line:
2657 if key:
2658 target = data
2659 continue
2660 line = line.rstrip(r'\\')
2661 if line[0] == '#':
2662 comment = True
2663 table_format='dat'
2664 target = key
2665 line = line.lstrip('#')
2666 elif comment:
2667 target = data
2668 if line[0:3] == 'RTH':
2669 target = key
2670 line = line[3:]
2671 table_format='rtai'
2672 elif line[0:3] == 'RTD':
2673 target = data
2674 line = line[3:]
2675 table_format='rtai'
2676 if (line[0:3] == '|--' or line[0:3] == '|:-') and \
2677 (line[-3:] == '--|' or line[-3:] == '-:|'):
2678 if not data and not key:
2679 table_format='ascii'
2680 target = key
2681 continue
2682 elif not key:
2683 table_format='md'
2684 key = data
2685 data = []
2686 target = data
2687 continue
2688 elif not data:
2689 target = data
2690 continue
2691 else:
2692 break
2693 target.append(line)
2694 else:
2695 break
2696 if len(data) > 5:
2697 break
2698 # find column separator of data and number of columns:
2699 col_seps = ['|', ',', ';', ':', '\t', '&', None]
2700 colstd = np.zeros(len(col_seps))
2701 colnum = np.zeros(len(col_seps), dtype=int)
2702 for k, sep in enumerate(col_seps):
2703 cols = []
2704 s = 5 if len(data) >= 8 else len(data) - 3
2705 if s < 0 or key:
2706 s = 0
2707 for line in data[s:]:
2708 cs = line.strip().split(sep)
2709 if not cs[0]:
2710 cs = cs[1:]
2711 if cs and not cs[-1]:
2712 cs = cs[:-1]
2713 cols.append(len(cs))
2714 colstd[k] = np.std(cols)
2715 colnum[k] = np.median(cols)
2716 if np.max(colnum) < 2:
2717 sep = None
2718 colnum = 1
2719 else:
2720 ci = np.where(np.array(colnum)>1.5)[0]
2721 ci = ci[np.argmin(colstd[ci])]
2722 sep = col_seps[ci]
2723 colnum = int(colnum[ci])
2724 # fix key:
2725 if not key and sep is not None and sep in ',;:\t|':
2726 table_format = 'csv'
2727 # read key:
2728 key_cols = []
2729 key_indices = []
2730 for line in key:
2731 cols, indices = read_key_line(line, sep, table_format)
2732 key_cols.append(cols)
2733 key_indices.append(indices)
2734 if not key_cols:
2735 # no obviously marked table key:
2736 key_num = 0
2737 for line in data:
2738 cols, indices = read_key_line(line, sep, table_format)
2739 numbers = 0
2740 for c in cols:
2741 try:
2742 v = float(c)
2743 numbers += 1
2744 except ValueError:
2745 pass
2746 if numbers == 0:
2747 key_cols.append(cols)
2748 key_indices.append(indices)
2749 key_num += 1
2750 else:
2751 break
2752 data = data[key_num:]
2753 kr = len(key_cols)-1
2754 # check for key with column indices:
2755 if kr >= 0:
2756 cols = key_cols[kr]
2757 numrow = True
2758 try:
2759 pv = int(cols[0])
2760 for c in cols[1:]:
2761 v = int(c)
2762 if v != pv+1:
2763 numrow = False
2764 break
2765 pv = v
2766 except ValueError:
2767 try:
2768 pv = aa2index(cols[0])
2769 for c in cols[1:]:
2770 v = aa2index(c)
2771 if v != pv+1:
2772 numrow = False
2773 break
2774 pv = v
2775 except ValueError:
2776 numrow = False
2777 if numrow:
2778 kr -= 1
2779 # check for unit line:
2780 units = None
2781 if kr > 0 and len(key_cols[kr]) == len(key_cols[kr-1]):
2782 units = key_cols[kr]
2783 kr -= 1
2784 # column labels:
2785 if kr >= 0:
2786 if units is None:
2787 # units may be part of the label:
2788 labels = []
2789 units = []
2790 for c in key_cols[kr]:
2791 if c[-1] == ')':
2792 lu = c[:-1].split('(')
2793 if len(lu) >= 2:
2794 labels.append(lu[0].strip())
2795 units.append('('.join(lu[1:]).strip())
2796 continue
2797 lu = c.split('/')
2798 if len(lu) >= 2:
2799 labels.append(lu[0].strip())
2800 units.append('/'.join(lu[1:]).strip())
2801 else:
2802 labels.append(c)
2803 units.append('')
2804 else:
2805 labels = key_cols[kr]
2806 indices = key_indices[kr]
2807 # init table columns:
2808 for k in range(colnum):
2809 self.append(labels[k], units[k], '%g')
2810 # read in sections:
2811 while kr > 0:
2812 kr -= 1
2813 for sec_label, sec_inx in zip(key_cols[kr], key_indices[kr]):
2814 col_inx = indices.index(sec_inx)
2815 self.header[col_inx].append(sec_label)
2816 if self.nsecs < len(self.header[col_inx])-1:
2817 self.nsecs = len(self.header[col_inx])-1
2818 # read data:
2819 post = np.zeros(colnum)
2820 precd = np.zeros(colnum)
2821 alld = np.zeros(colnum)
2822 numc = [False] * colnum
2823 exped = [True] * colnum
2824 fixed = [True] * colnum
2825 strf = [False] * colnum
2826 nans = [None] * colnum
2827 for line in data:
2828 read_data_line(line, sep, post, precd, alld, numc, exped, fixed,
2829 strf, missing, nans)
2830 # read remaining data:
2831 for line in fh:
2832 line = line.rstrip()
2833 if line == stop:
2834 break;
2835 if table_format == 'tex':
2836 if r'\end{tabular' in line or r'\hline' in line:
2837 break
2838 line = line.rstrip(r'\\')
2839 if (line[0:3] == '|--' or line[0:3] == '|:-') and \
2840 (line[-3:] == '--|' or line[-3:] == '-:|'):
2841 break
2842 if line[0:3] == 'RTD':
2843 line = line[3:]
2844 read_data_line(line, sep, post, precd, alld, numc, exped, fixed,
2845 strf, missing, nans)
2846 # set formats:
2847 for k in range(len(alld)):
2848 if strf[k]:
2849 self.set_format('%%-%ds' % alld[k], k)
2850 # make sure all elements are strings:
2851 for i in range(len(self.data[k])):
2852 if self.data[k][i] is np.nan:
2853 self.data[k][i] = nans[k]
2854 else:
2855 self.data[k][i] = str(self.data[k][i])
2856 elif exped[k]:
2857 self.set_format('%%%d.%de' % (alld[k], post[k]), k)
2858 elif fixed[k]:
2859 self.set_format('%%%d.%df' % (alld[k], post[k]), k)
2860 else:
2861 self.set_format('%%%d.%dg' % (alld[k], precd[k]), k)
2862 # close file:
2863 if own_file:
2864 fh.close()
2867def write(fh, data, header, units=None, formats=None,
2868 table_format=None, delimiter=None, unit_style=None,
2869 column_numbers=None, sections=None, align_columns=None,
2870 shrink_width=True, missing=default_missing_str,
2871 center_columns=False, latex_label_command='',
2872 latex_merge_std=False):
2873 """Construct table and write to file.
2875 Parameters
2876 ----------
2877 fh: filename or stream
2878 If not a stream, the file with name `fh` is opened.
2879 If `fh` does not have an extension,
2880 the `table_format` is appended as an extension.
2881 Otherwise `fh` is used as a stream for writing.
2882 data: 1-D or 2-D ndarray of data
2883 The data of the table.
2884 header: list of str
2885 Header labels for each column.
2886 units: list of str, optional
2887 Unit strings for each column.
2888 formats: str or list of str, optional
2889 Format strings for each column. If only a single format string is
2890 given, then all columns are initialized with this format string.
2892 See `TableData.write()` for a description of all other parameters.
2894 Example
2895 -------
2896 ```
2897 write(sys.stdout, np.random.randn(4,3), ['aaa', 'bbb', 'ccc'], units=['m', 's', 'g'], formats='%.2f')
2898 ```
2899 """
2900 td = TableData(data, header, units, formats)
2901 td.write(fh, table_format=table_format, unit_style=unit_style,
2902 column_numbers=column_numbers, missing=missing,
2903 shrink_width=shrink_width, delimiter=delimiter,
2904 align_columns=align_columns, sections=sections,
2905 latex_label_command=latex_label_command,
2906 latex_merge_std=latex_merge_std)
2909def add_write_table_config(cfg, table_format=None, delimiter=None,
2910 unit_style=None, column_numbers=None,
2911 sections=None, align_columns=None,
2912 shrink_width=True, missing='-',
2913 center_columns=False,
2914 latex_label_command='',
2915 latex_merge_std=False):
2916 """Add parameter specifying how to write a table to a file as a new
2917section to a configuration.
2919 Parameters
2920 ----------
2921 cfg: ConfigFile
2922 The configuration.
2923 """
2925 cfg.add_section('File format for storing analysis results:')
2926 cfg.add('fileFormat', table_format or 'auto', '', 'Default file format used to store analysis results.\nOne of %s.' % ', '.join(TableData.formats))
2927 cfg.add('fileDelimiter', delimiter or 'auto', '', 'String used to separate columns or "auto".')
2928 cfg.add('fileUnitStyle', unit_style or 'auto', '', 'Add units as extra row ("row"), add units to header label separated by "/" ("header"), do not print out units ("none"), or "auto".')
2929 cfg.add('fileColumnNumbers', column_numbers or 'none', '', 'Add line with column indices ("index", "num", "aa", "AA", or "none")')
2930 cfg.add('fileSections', sections or 'auto', '', 'Maximum number of section levels or "auto"')
2931 cfg.add('fileAlignColumns', align_columns or 'auto', '', 'If True, write all data of a column using the same width, if False write the data without any white space, or "auto".')
2932 cfg.add('fileShrinkColumnWidth', shrink_width, '', 'Allow to make columns narrower than specified by the corresponding format strings.')
2933 cfg.add('fileMissing', missing, '', 'String used to indicate missing data values.')
2934 cfg.add('fileCenterColumns', center_columns, '', 'Center content of all columns instead of left align columns of strings and right align numbers (markdown, html, and latex).')
2935 cfg.add('fileLaTeXLabelCommand', latex_label_command, '', 'LaTeX command name for formatting column labels of the table header.')
2936 cfg.add('fileLaTeXMergeStd', latex_merge_std, '', 'Merge header of columns with standard deviations with previous column (LaTeX tables only).')
2939def write_table_args(cfg):
2940 """Translates a configuration to the respective parameter names for
2941writing a table to a file.
2943 The return value can then be passed as key-word arguments to TableData.write().
2945 Parameters
2946 ----------
2947 cfg: ConfigFile
2948 The configuration.
2950 Returns
2951 -------
2952 a: dict
2953 Dictionary with names of arguments of the `TableData.write` function
2954 and their values as supplied by `cfg`.
2955 """
2956 d = cfg.map({'table_format': 'fileFormat',
2957 'delimiter': 'fileDelimiter',
2958 'unit_style': 'fileUnitStyle',
2959 'column_numbers': 'fileColumnNumbers',
2960 'sections': 'fileSections',
2961 'align_columns': 'fileAlignColumns',
2962 'shrink_width': 'fileShrinkColumnWidth',
2963 'missing': 'fileMissing',
2964 'center_columns': 'fileCenterColumns',
2965 'latex_label_command': 'fileLaTeXLabelCommand',
2966 'latex_merge_std': 'fileLaTeXMergeStd'})
2967 if 'sections' in d:
2968 if d['sections'] != 'auto':
2969 d['sections'] = int(d['sections'])
2970 return d
2973def latex_unit(unit):
2974 """Translate unit string into SIunit LaTeX code.
2976 Parameters
2977 ----------
2978 unit: str
2979 String enoting a unit.
2981 Returns
2982 -------
2983 unit: str
2984 Unit string as valid LaTeX code.
2985 """
2986 si_prefixes = {'y': '\\yocto',
2987 'z': '\\zepto',
2988 'a': '\\atto',
2989 'f': '\\femto',
2990 'p': '\\pico',
2991 'n': '\\nano',
2992 'u': '\\micro',
2993 'm': '\\milli',
2994 'c': '\\centi',
2995 'd': '\\deci',
2996 'h': '\\hecto',
2997 'k': '\\kilo',
2998 'M': '\\mega',
2999 'G': '\\giga',
3000 'T': '\\tera',
3001 'P': '\\peta',
3002 'E': '\\exa',
3003 'Z': '\\zetta',
3004 'Y': '\\yotta' }
3005 si_units = {'m': '\\metre',
3006 'g': '\\gram',
3007 's': '\\second',
3008 'A': '\\ampere',
3009 'K': '\\kelvin',
3010 'mol': '\\mole',
3011 'cd': '\\candela',
3012 'Hz': '\\hertz',
3013 'N': '\\newton',
3014 'Pa': '\\pascal',
3015 'J': '\\joule',
3016 'W': '\\watt',
3017 'C': '\\coulomb',
3018 'V': '\\volt',
3019 'F': '\\farad',
3020 'O': '\\ohm',
3021 'S': '\\siemens',
3022 'Wb': '\\weber',
3023 'T': '\\tesla',
3024 'H': '\\henry',
3025 'C': '\\celsius',
3026 'lm': '\\lumen',
3027 'lx': '\\lux',
3028 'Bq': '\\becquerel',
3029 'Gv': '\\gray',
3030 'Sv': '\\sievert'}
3031 other_units = {"'": '\\arcminute',
3032 "''": '\\arcsecond',
3033 'a': '\\are',
3034 'd': '\\dday',
3035 'eV': '\\electronvolt',
3036 'ha': '\\hectare',
3037 'h': '\\hour',
3038 'L': '\\liter',
3039 'l': '\\litre',
3040 'min': '\\minute',
3041 'Np': '\\neper',
3042 'rad': '\\rad',
3043 't': '\\ton',
3044 '%': '\\%'}
3045 unit_powers = {'^2': '\\squared',
3046 '^3': '\\cubed',
3047 '/': '\\per',
3048 '^-1': '\\power{}{-1}',
3049 '^-2': '\\rpsquared',
3050 '^-3': '\\rpcubed'}
3051 if '\\' in unit: # this string is already translated!
3052 return unit
3053 units = ''
3054 j = len(unit)
3055 while j >= 0:
3056 for k in range(-3, 0):
3057 if j+k < 0:
3058 continue
3059 uss = unit[j+k:j]
3060 if uss in unit_powers:
3061 units = unit_powers[uss] + units
3062 break
3063 elif uss in other_units:
3064 units = other_units[uss] + units
3065 break
3066 elif uss in si_units:
3067 units = si_units[uss] + units
3068 j = j+k
3069 k = 0
3070 if j-1 >= 0:
3071 uss = unit[j-1:j]
3072 if uss in si_prefixes:
3073 units = si_prefixes[uss] + units
3074 k = -1
3075 break
3076 else:
3077 k = -1
3078 units = unit[j+k:j] + units
3079 j = j + k
3080 return units
3083def index2aa(n, a='a'):
3084 """Convert an integer into an alphabetical representation.
3086 The integer number is converted into 'a', 'b', 'c', ..., 'z',
3087 'aa', 'ab', 'ac', ..., 'az', 'ba', 'bb', ...
3089 Inspired by https://stackoverflow.com/a/37604105
3091 Parameters
3092 ----------
3093 n: int
3094 An integer to be converted into alphabetical representation.
3095 a: str ('a' or 'A')
3096 Use upper or lower case characters.
3098 Returns
3099 -------
3100 ns: str
3101 Alphabetical represtnation of an integer.
3102 """
3103 d, m = divmod(n, 26)
3104 bm = chr(ord(a)+m)
3105 return index2aa(d-1, a) + bm if d else bm
3108def aa2index(s):
3109 """Convert an alphabetical representation to an index.
3111 The alphabetical representation 'a', 'b', 'c', ..., 'z',
3112 'aa', 'ab', 'ac', ..., 'az', 'ba', 'bb', ...
3113 is converted to an index starting with 0.
3115 Parameters
3116 ----------
3117 s: str
3118 Alphabetical representation of an index.
3120 Returns
3121 -------
3122 index: int
3123 The corresponding index.
3125 Raises
3126 ------
3127 ValueError:
3128 Invalid character in input string.
3129 """
3130 index = 0
3131 maxc = ord('z') - ord('a') + 1
3132 for c in s.lower():
3133 index *= maxc
3134 if ord(c) < ord('a') or ord(c) > ord('z'):
3135 raise ValueError('invalid character "%s" in string.' % c)
3136 index += ord(c) - ord('a') + 1
3137 return index-1
3140class IndentStream(object):
3141 """Filter an output stream and start each newline with a number of
3142 spaces.
3143 """
3144 def __init__(self, stream, indent=4):
3145 self.stream = stream
3146 self.indent = indent
3147 self.pending = True
3149 def __getattr__(self, attr_name):
3150 return getattr(self.stream, attr_name)
3152 def write(self, data):
3153 if not data:
3154 return
3155 if self.pending:
3156 self.stream.write(' '*self.indent)
3157 self.pending = False
3158 substr = data.rstrip('\n')
3159 rn = len(data) - len(substr)
3160 if len(substr) > 0:
3161 self.stream.write(substr.replace('\n', '\n'+' '*self.indent))
3162 if rn > 0:
3163 self.stream.write('\n'*rn)
3164 self.pending = True
3166 def flush(self):
3167 self.stream.flush()
3170def main():
3171 # setup a table:
3172 df = TableData()
3173 df.append(["data", "partial information", "ID"], "", "%-s", list('ABCDEFGH'))
3174 df.append("size", "m", "%6.2f", [2.34, 56.7, 8.9])
3175 df.append("full weight", "kg", "%.0f", 122.8)
3176 df.append_section("complete reaction")
3177 df.append("speed", "m/s", "%.3g", 98.7)
3178 df.append("median jitter", "mm", "%.1f", 23)
3179 df.append("size", "g", "%.2e", 1.234)
3180 df.append_data(np.nan, 2) # single value
3181 df.append_data((0.543, 45, 1.235e2)) # remaining row
3182 df.append_data((43.21, 6789.1, 3405, 1.235e-4), 2) # next row
3183 a = 0.5*np.arange(1, 6)*np.random.randn(5, 5) + 10.0 + np.arange(5)
3184 df.append_data(a.T, 1) # rest of table
3185 df[3:6,'weight'] = [11.0]*3
3187 # write out in all formats:
3188 for tf in TableData.formats:
3189 print(' - `%s`: %s' % (tf, TableData.descriptions[tf]))
3190 print(' ```')
3191 iout = IndentStream(sys.stdout, 4+2)
3192 df.write(iout, table_format=tf)
3193 print(' ```')
3194 print('')
3197if __name__ == "__main__":
3198 main()