Package lib :: Package text :: Module table
[hide private]
[frames] | no frames]

Source Code for Module lib.text.table

  1  ############################################################################### 
  2  #                                                                             # 
  3  # Copyright (C) 2009-2013 Edward d'Auvergne                                   # 
  4  #                                                                             # 
  5  # This file is part of the program relax (http://www.nmr-relax.com).          # 
  6  #                                                                             # 
  7  # This program is free software: you can redistribute it and/or modify        # 
  8  # it under the terms of the GNU General Public License as published by        # 
  9  # the Free Software Foundation, either version 3 of the License, or           # 
 10  # (at your option) any later version.                                         # 
 11  #                                                                             # 
 12  # This program is distributed in the hope that it will be useful,             # 
 13  # but WITHOUT ANY WARRANTY; without even the implied warranty of              # 
 14  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               # 
 15  # GNU General Public License for more details.                                # 
 16  #                                                                             # 
 17  # You should have received a copy of the GNU General Public License           # 
 18  # along with this program.  If not, see <http://www.gnu.org/licenses/>.       # 
 19  #                                                                             # 
 20  ############################################################################### 
 21   
 22  # Module docstring. 
 23  """Functions for the text formatting of tables.""" 
 24   
 25  # Python module imports. 
 26  from copy import deepcopy 
 27  from textwrap import wrap 
 28   
 29  # relax module imports. 
 30  from lib.check_types import is_float 
 31  from lib.errors import RelaxError 
 32   
 33   
 34  # Special variables. 
 35  MULTI_COL = "@@MULTI@@" 
 36   
 37   
38 -def _blank(width=None, prefix=' ', postfix=' '):
39 """Create a blank line for the table. 40 41 @keyword width: The total width of the table. 42 @type width: int 43 @keyword prefix: The text to add to the start of the line. 44 @type prefix: str 45 @keyword postfix: The text to add to the end of the line. 46 @type postfix: str 47 @return: The rule. 48 @rtype: str 49 """ 50 51 # Return the blank line. 52 return prefix + ' '*width + postfix + "\n"
53 54
55 -def _convert_to_string(data=None, justification=None, custom_format=None):
56 """Convert all elements of the given data structures to strings in place. 57 58 @keyword data: The headings or content to convert. 59 @type data: list of lists 60 @keyword justification: The structure to store the cell justification in. 61 @type justification: list of lists 62 @keyword custom_format: This list allows a custom format to be specified for each column. The number of elements must match the number of columns. If an element is None, then the default will be used. Otherwise the elements must be valid string formatting constructs. 63 @type custom_format: None or list of None and str 64 """ 65 66 # Loop over the rows. 67 for i in range(len(data)): 68 # Loop over the columns. 69 for j in range(len(data[i])): 70 # Skip multi-columns. 71 if data[i][j] == MULTI_COL: 72 continue 73 74 # Default left justification. 75 justification[i][j] = 'l' 76 77 # Right justify numbers. 78 if not isinstance(data[i][j], bool) and (isinstance(data[i][j], int) or is_float(data[i][j])): 79 justification[i][j] = 'r' 80 81 # None types. 82 if data[i][j] == None: 83 data[i][j] = '' 84 85 # Custom format (defaulting to standard string conversion if all fails). 86 elif custom_format and custom_format[j]: 87 try: 88 data[i][j] = custom_format[j] % data[i][j] 89 except TypeError: 90 data[i][j] = "%s" % data[i][j] 91 92 # Bool types. 93 elif isinstance(data[i][j], bool): 94 data[i][j] = "%s" % data[i][j] 95 96 # Int types. 97 elif isinstance(data[i][j], int): 98 data[i][j] = "%i" % data[i][j] 99 100 # Float types. 101 elif is_float(data[i][j]): 102 data[i][j] = "%g" % data[i][j] 103 104 # All other non-string types. 105 elif not isinstance(data[i][j], str): 106 data[i][j] = "%s" % data[i][j]
107 108
109 -def _determine_widths(data=None, widths=None, separator=None):
110 """Determine the maximum column widths needed given the data. 111 112 @keyword data: Either the headings or content converted to strings to check the widths of. 113 @type data: list of lists of str 114 @keyword widths: The list of widths to start with. If data is found to be wider than this list, then the width of that column will be expanded. 115 @type widths: list of int 116 @keyword separator: The column separation string. 117 @type separator: str 118 """ 119 120 # The number of rows and columns. 121 num_rows = len(data) 122 num_cols = len(data[0]) 123 124 # Determine the maximum column widths. 125 multi_col = False 126 for i in range(num_rows): 127 for j in range(num_cols): 128 # Switch the flag. 129 if data[i][j] == MULTI_COL: 130 multi_col = True 131 132 # Skip multicolumn entries. 133 if data[i][j] == MULTI_COL or (j < num_cols-1 and data[i][j+1] == MULTI_COL): 134 continue 135 136 # The element is larger than the previous. 137 if len(data[i][j]) > widths[j]: 138 widths[j] = len(data[i][j]) 139 140 # Handle overfull multi-column cells. 141 if multi_col: 142 for i in range(num_rows): 143 for j in range(num_cols): 144 # End of multicolumn cell. 145 if data[i][j] == MULTI_COL and (j == num_cols-1 or (j < num_cols-1 and data[i][j+1] != MULTI_COL)): 146 col_sum_width = widths[j] 147 while True: 148 # Walk back. 149 for k in range(j-1, -1, -1): 150 col_sum_width += len(separator) + widths[k] 151 152 # Out of the cell. 153 if data[i][k] != MULTI_COL: 154 break 155 156 # Nothing more to do. 157 break 158 159 # The multicolumn width. 160 multi_col_width = len(data[i][k]) 161 162 # The multicolumn cell is wider than the columns it spans, so expand the last column. 163 if multi_col_width > col_sum_width: 164 widths[j] += multi_col_width - col_sum_width
165 166
167 -def _rule(width=None, prefix=' ', postfix=' '):
168 """Create a horizontal rule for the table. 169 170 @keyword width: The total width of the table. 171 @type width: int 172 @keyword prefix: The text to add to the start of the line. 173 @type prefix: str 174 @keyword postfix: The text to add to the end of the line. 175 @type postfix: str 176 @return: The rule. 177 @rtype: str 178 """ 179 180 # Return the rule. 181 return prefix + '_'*width + postfix + "\n"
182 183
184 -def _table_line(text=None, widths=None, separator=' ', pad_left=' ', pad_right=' ', prefix=' ', postfix=' ', justification=None):
185 """Format a line of a table. 186 187 @keyword text: The list of table elements. If not given, an empty line will be be produced. 188 @type text: list of str or None 189 @keyword widths: The list of column widths for the table. 190 @type widths: list of int 191 @keyword separator: The column separation string. 192 @type separator: str 193 @keyword pad_left: The string to pad the left side of the table with. 194 @type pad_left: str 195 @keyword pad_right: The string to pad the right side of the table with. 196 @type pad_right: str 197 @keyword prefix: The text to add to the start of the line. 198 @type prefix: str 199 @keyword postfix: The text to add to the end of the line. 200 @type postfix: str 201 @keyword justification: The cell justification structure. The elements should be 'l' for left justification and 'r' for right. 202 @type justification: list of str 203 @return: The table line. 204 @rtype: str 205 """ 206 207 # Initialise. 208 line = prefix + pad_left 209 num_col = len(widths) 210 211 # Loop over the columns. 212 for i in range(num_col): 213 # Multicolumn (middle/end). 214 if text[i] == MULTI_COL: 215 continue 216 217 # The column separator. 218 if i > 0: 219 line += separator 220 221 # Multicolumn (start). 222 if i < num_col-1 and text[i+1] == MULTI_COL: 223 # Find the full multicell width. 224 width = widths[i] 225 for j in range(i+1, num_col): 226 if text[j] == MULTI_COL: 227 width += len(separator) + widths[j] 228 else: 229 break 230 231 # Add the padded text. 232 if justification[i] == 'l': 233 line += text[i] 234 line += " " * (width - len(text[i])) 235 if justification[i] == 'r': 236 line += text[i] 237 238 # Normal cell. 239 else: 240 if justification[i] == 'l': 241 line += text[i] 242 line += " " * (widths[i] - len(text[i])) 243 if justification[i] == 'r': 244 line += text[i] 245 246 # Close the line. 247 line += pad_right + postfix + "\n" 248 249 # Return the text. 250 return line
251 252
253 -def format_table(headings=None, contents=None, max_width=None, separator=' ', pad_left=' ', pad_right=' ', prefix=' ', postfix=' ', custom_format=None, spacing=False, debug=False):
254 """Format and return the table as text. 255 256 If the heading or contents contains the value of the MULTI_COL constant defined in this module, then that cell will be merged with the previous cell to allow elements to span multiple columns. 257 258 259 @keyword headings: The table header. 260 @type headings: list of lists of str 261 @keyword contents: The table contents. 262 @type contents: list of lists of str 263 @keyword max_width: The maximum width of the table. 264 @type max_width: int 265 @keyword separator: The column separation string. 266 @type separator: str 267 @keyword pad_left: The string to pad the left side of the table with. 268 @type pad_left: str 269 @keyword pad_right: The string to pad the right side of the table with. 270 @type pad_right: str 271 @keyword prefix: The text to add to the start of the line. 272 @type prefix: str 273 @keyword postfix: The text to add to the end of the line. 274 @type postfix: str 275 @keyword custom_format: This list allows a custom format to be specified for each column. The number of elements must match the number of columns. If an element is None, then the default will be used. Otherwise the elements must be valid string formatting constructs. 276 @type custom_format: None or list of None and str 277 @keyword spacing: A flag which if True will add blank line between each row. 278 @type spacing: bool 279 @keyword debug: A flag which if True will activate a number of debugging printouts. 280 @type debug: bool 281 @return: The formatted table. 282 @rtype: str 283 """ 284 285 # Initialise some variables. 286 text = '' 287 num_rows = len(contents) 288 num_cols = len(contents[0]) 289 if headings != None: 290 num_head_rows = len(headings) 291 292 # Column number checks. 293 if custom_format != None and len(custom_format) != num_cols: 294 raise RelaxError("The number of columns is %s but the number of elements in custom_format is %s." % (num_cols, len(custom_format))) 295 if headings != None: 296 for i in range(num_head_rows): 297 if len(headings[i]) != num_cols: 298 raise RelaxError("The %s columns does not match the %s elements in the heading row %s." % (num_cols, len(headings[i]), headings[i])) 299 for i in range(num_rows): 300 if len(contents[i]) != num_cols: 301 raise RelaxError("The %s columns does not match the %s elements in the contents row %s." % (num_cols, len(contents[i]), contents[i])) 302 303 304 # Deepcopy so that modifications to the data are not seen. 305 if headings != None: 306 headings = deepcopy(headings) 307 contents = deepcopy(contents) 308 309 # Create data structures for specifying the cell justification. 310 if headings != None: 311 justification_headings = deepcopy(headings) 312 justification_contents = deepcopy(contents) 313 314 # Convert all data to strings. 315 if headings != None: 316 _convert_to_string(data=headings, justification=justification_headings, custom_format=custom_format) 317 _convert_to_string(data=contents, justification=justification_contents, custom_format=custom_format) 318 319 # Determine the pre-wrapping column widths. 320 prewrap_widths = [0] * num_cols 321 if headings != None: 322 data = headings + contents 323 else: 324 data = contents 325 _determine_widths(data=data, widths=prewrap_widths, separator=separator) 326 327 # The free space for the text (subtracting the space used for the formatting). 328 used = len(pad_left) 329 used += len(pad_right) 330 used += len(separator) * (num_cols - 1) 331 if max_width: 332 free_space = max_width - used 333 else: 334 free_space = 1000 335 336 # The maximal width for all cells. 337 free_width = sum(prewrap_widths) 338 339 # Column wrapping. 340 if free_width > free_space: 341 # Debugging printouts. 342 if debug: 343 print 344 print("Table column wrapping algorithm:") 345 print("%-20s %s" % ("num_cols:", num_cols)) 346 print("%-20s %s" % ("free space:", free_space)) 347 348 # New structures. 349 new_widths = deepcopy(prewrap_widths) 350 num_cols_wrap = num_cols 351 free_space_wrap = free_space 352 col_wrap = [True] * num_cols 353 354 # Loop. 355 while True: 356 # The average column width. 357 ave_width = int(free_space_wrap / num_cols_wrap) 358 359 # Debugging printout. 360 if debug: 361 print(" %-20s %s" % ("ave_width:", ave_width)) 362 363 # Rescale. 364 rescale = False 365 for i in range(num_cols): 366 # Remove the column from wrapping if smaller than the average wrapped width. 367 if col_wrap[i] and new_widths[i] < ave_width: 368 # Recalculate. 369 free_space_wrap = free_space_wrap - new_widths[i] 370 num_cols_wrap -= 1 371 rescale = True 372 373 # Remove the column from wrapping. 374 col_wrap[i] = False 375 376 # Debugging printout. 377 if debug: 378 print(" %-20s %s" % ("remove column:", i)) 379 380 # Done. 381 if not rescale: 382 # Set the column widths. 383 for i in range(num_cols): 384 if new_widths[i] > ave_width: 385 new_widths[i] = ave_width 386 break 387 388 # Debugging printouts. 389 if debug: 390 print(" %-20s %s" % ("prewrap_widths:", prewrap_widths)) 391 print(" %-20s %s" % ("new_widths:", new_widths)) 392 print(" %-20s %s" % ("num_cols:", num_cols)) 393 print(" %-20s %s" % ("num_cols_wrap:", num_cols_wrap)) 394 print(" %-20s %s" % ("free_space:", free_space)) 395 print(" %-20s %s" % ("free_space_wrap:", free_space_wrap)) 396 print(" %-20s %s" % ("col_wrap:", col_wrap)) 397 398 # No column wrapping. 399 else: 400 new_widths = prewrap_widths 401 col_wrap = [False] * num_cols 402 403 # The total table width. 404 total_width = sum(new_widths) + used 405 406 # The header. 407 if headings != None: 408 text += _rule(width=total_width) # Top rule. 409 text += _blank(width=total_width) # Blank line. 410 for i in range(num_head_rows): 411 text += _table_line(text=headings[i], widths=new_widths, separator=' ', pad_left=pad_left, pad_right=pad_right, prefix=prefix, postfix=postfix, justification=justification_headings[i]) 412 if i < num_head_rows-1 and spacing: 413 text += _blank(width=total_width) 414 text += _rule(width=total_width) # Middle rule. 415 416 # No header. 417 else: 418 text += _rule(width=total_width) # Top rule. 419 420 # The table contents. 421 for i in range(num_rows): 422 # Column text, with wrapping. 423 col_text = [contents[i]] 424 num_lines = 1 425 for j in range(num_cols): 426 if col_wrap[j]: 427 # Wrap. 428 lines = wrap(col_text[0][j], new_widths[j]) 429 430 # Count the lines. 431 num_lines = len(lines) 432 433 # Replace the column text. 434 for k in range(num_lines): 435 # New row of empty text. 436 if len(col_text) <= k: 437 col_text.append(['']*num_cols) 438 439 # Pack the data. 440 col_text[k][j] = lines[k] 441 442 # Blank line (between rows when asked, and for the first row after the header). 443 if spacing or i == 0: 444 text += _blank(width=total_width) 445 446 # The contents. 447 for k in range(num_lines): 448 text += _table_line(text=col_text[k], widths=new_widths, separator=separator, pad_left=pad_left, pad_right=pad_right, prefix=prefix, postfix=postfix, justification=justification_contents[i]) 449 450 # The bottom rule, followed by a blank line. 451 text += _rule(width=total_width) 452 text += _blank(width=total_width) 453 454 # Return the table text. 455 return text
456