Attachment 'SortBy-1.7.py'

Download

   1 """
   2     MoinMoin - SortBy Macro
   3     Copyright (c) 2001 by Fred Bremmer <Fred.Bremmer@ubc.ca>
   4     Modified (c) 2007 by Lane Rettig <Lane.Rettig@deshaw.com>
   5 
   6     Based on an earlier SortBy Macro
   7     Copyright (c) 2001 by Tim Bird <tbird@lineo.com>
   8     All rights reserved, see COPYING for details.
   9     
  10     Updated June 24, 2008 for compatibility with Moin 1.7.
  11     (code commented below).
  12 
  13 DESCRIPTION: Sort a table by one or more columns and sort types/orders
  14 
  15 SortBy(page,num_headers,column,sort_type[,othercolumn,sort_type]...)
  16 
  17 Examples:
  18 
  19     [[SortBy(TablePage,1,3,alpha)]]
  20 
  21 This would return the first table on TablePage sorted by column 3
  22 (column 1 is the leftmost column).  The rows would be sorted using a
  23 lexicographic (alpha) sort.  It would preserve 1 header row of the
  24 table, without sorting it.
  25 
  26     [[SortBy(TablePage,0,2,reversenumber,1,alpha)]]
  27 
  28 This would return the first table on TablePage sorted using a reverse
  29 numeric sort on column 2, but rows with identical values in column 2
  30 would be sorted in alpha order of their values in column 1.  It would
  31 not preserve the first row of the table.  The first row of the table 
  32 would be sorted also (zero header rows left unsorted).
  33 
  34 Supported sort types are: alpha, number, nocase, nosort, reversealpha,
  35 reversenumber, reversenocase, reversenosort
  36 
  37 'nosort' can be used to make the SortBy macro behave like an Include
  38 that only loads the first table from the page, not the whole page.
  39 Thus you can have comments on the table page that are not shown in the
  40 SortBy rendering on the including page.
  41 
  42 See SortByTest below for a more comprehensive list of examples.
  43 
  44 Utility functions:
  45     error()        - convert an error message to HTML
  46     read_table()   - read a file and return its first table's data
  47 
  48 Functions to convert a string into an appropriate sortable value:
  49     strip_fmt()    - remove wiki formatting and commas
  50     to_number()    - convert to a number if possible
  51     to_nocase()    - convert everything to the same case
  52     to_nosort()    - return ascending numbers regardless of arg
  53 
  54 Main loop functions:
  55     process_args() - parse args string and do some error checking
  56     sort_table()   - do the actual sorting of table rows
  57     format()       - generate the HTML output
  58     execute()      - the macro's main loop, called by MoinMoin
  59 
  60 """
  61 
  62 # After installing this macro, you can test it by creating a wiki page
  63 # named "SortByTest" which contains the following text:
  64 
  65 SortByTest = """
  66 [[TableOfContents]]
  67 
  68 == Original Table ==
  69 ||'''First'''	||'''Second'''	||'''Third'''	||
  70 ||A		||2,000,000	||zebra		||
  71 ||C		||1		||123		||
  72 ||b		||2		||word		||
  73 ||d		||30		||{{{word}}}	||
  74 ||A		||2		||Wordy		||
  75 ||B		||4.2		||23		||
  76 ||A		||2		||'''word'''	||
  77 
  78 == Column 1 No Sort ==
  79 {{{[[SortBy(SortByTest, 1, 1, nosort)]]}}}
  80 
  81 [[SortBy(SortByTest, 1, 1, nosort)]]
  82 
  83 This simply includes the first table on the source page at this point in the
  84 page containing the macro. Rows are in the same order as the original table.
  85 
  86 == Column 1 Reverse No Sort ==
  87 {{{[[SortBy(SortByTest, 1, 1, reversenosort)]]}}}
  88 
  89 [[SortBy(SortByTest, 1, 1, reversenosort)]]
  90 
  91 Similar to the previous example, but now all rows except the one header row
  92 are in reverse order.
  93 
  94 == Column 1 Alphabetical ==
  95 {{{[[SortBy(SortByTest, 1, 1, alpha)]]}}}
  96 
  97 [[SortBy(SortByTest, 1, 1, alpha)]]
  98 
  99 In alphabetical order all uppercase letters are sorted before lowercase
 100 letters.
 101 
 102 == Column 1 Reverse Alphabetical ==
 103 {{{[[SortBy(SortByTest, 1, 1, reversealpha)]]}}}
 104 
 105 [[SortBy(SortByTest, 1, 1, reversealpha)]]
 106 
 107 Prepending 'reverse' to the sort-type changes the order from ascending to
 108 descending.
 109 
 110 == Column 1 Case-Insensitive ==
 111 {{{[[SortBy(SortByTest, 1, 1, nocase)]]}}}
 112 
 113 [[SortBy(SortByTest, 1, 1, nocase)]]
 114 
 115 Now 'B' and 'b' are considered equal, and are presented in the order they
 116 appear in the original table.
 117 
 118 == Column 2 Numerical ==
 119 {{{[[SortBy(SortByTest, 1, 2, number)]]}}}
 120 
 121 [[SortBy(SortByTest, 1, 2, number)]]
 122 
 123 As you would expect, 30 comes before 2,000,000. Commas are ignored for
 124 sorting. Integers and floating point can both be used, and they sort
 125 correctly.
 126 
 127 == Column 2 Alphabetical ==
 128 {{{[[SortBy(SortByTest, 1, 2, alpha)]]}}}
 129 
 130 [[SortBy(SortByTest, 1, 2, alpha)]]
 131 
 132 In 'alphabetical' order, 2 comes before 3, so 2,000,000 sorts before 30.
 133 
 134 == Column 2 Reverse Numerical ==
 135 {{{[[SortBy(SortByTest, 1, 2, reversenumber)]]}}}
 136 
 137 [[SortBy(SortByTest, 1, 2, reversenumber)]]
 138 
 139 Again, prepending 'reverse' to the sort-type changes the order from ascending
 140 to descending.
 141 
 142 == Column 3 Alphabetical ==
 143 {{{[[SortBy(SortByTest, 1, 3, alpha)]]}}}
 144 
 145 [[SortBy(SortByTest, 1, 3, alpha)]]
 146 
 147 Wiki formatting is ignored, and the sort is stable. The variations of 'word'
 148 are simply listed in the same order as they appear in the original table.
 149 
 150 == Column 3 Numerical ==
 151 {{{[[SortBy(SortByTest, 1, 3, number)]]}}}
 152 
 153 [[SortBy(SortByTest, 1, 3, number)]]
 154 
 155 Now 23 comes before 123, and non-numeric values are sorted in alphabetical
 156 order as above.
 157 
 158 == Column 1 Alphabetical, Column 2 Numerical, Column 3 Alphabetical ==
 159 {{{[[SortBy(SortByTest, 1, 1, alpha, 2, number, 3, alpha)]]}}}
 160 
 161 [[SortBy(SortByTest, 1, 1, alpha, 2, number, 3, alpha)]]
 162 
 163 The main sort is on column 1, but within the duplicate 'A's the 2's and 
 164 2,000,000 are sorted, and within the 2's the values in column 3 are in 
 165 case-sensitive alphabetical order.
 166 
 167 == Column 1 Case-Insensitive, Column 2 Numerical, Column 3 Case-Insensitive ==
 168 {{{[[SortBy(SortByTest, 1, 1, nocase, 2, number, 3, nocase)]]}}}
 169 
 170 [[SortBy(SortByTest, 1, 1, nocase, 2, number, 3, nocase)]]
 171 
 172 The main sort is still on column 1, but within the case-insensitive duplicate
 173 'b's the values in column 2 are in numerical order. Also, within the duplicate
 174 A-2 rows, column 3 is case-insensitive, so now Wordy follows word.
 175 
 176 == Zero Header Rows, Case-Insensitive Sort on Column 1 ==
 177 {{{[[SortBy(SortByTest, 0, 1, nocase)]]}}}
 178 
 179 [[SortBy(SortByTest, 0, 1, nocase)]]
 180 
 181 The first row is sorted along with all the other rows.
 182 
 183 == Three Header Rows, Case-Insensitive Sort on Column 1 ==
 184 {{{[[SortBy(SortByTest, 3, 1, nocase)]]}}}
 185 
 186 [[SortBy(SortByTest, 3, 1, nocase)]]
 187 
 188 The first three rows are not sorted along with all the other rows.
 189 
 190 == Invalid Argument Examples ==
 191 
 192 [[SortBy(NonExistentPage, 1, 1, nocase)]]
 193 
 194 [[SortBy(SortByTest, 1, 1)]]
 195 
 196 [[SortBy(SortByTest, x, 1, alpha)]]
 197 
 198 [[SortBy(SortByTest, 1, x, alpha)]]
 199 
 200 [[SortBy(SortByTest, -1, 1, alpha)]]
 201 
 202 [[SortBy(SortByTest, 10, 1, alpha)]]
 203 
 204 [[SortBy(SortByTest, 1, 0, alpha)]]
 205 
 206 [[SortBy(SortByTest, 1, 10, alpha)]]
 207 
 208 [[SortBy(SortByTest, 1, 1, x)]]
 209 
 210 [[SortBy(SortByTest, 1, 1, alpha, 1)]]
 211 
 212 """
 213 
 214 import sys, re, StringIO, cgi
 215 from MoinMoin.Page import Page
 216 
 217 
 218 class SortByError(Exception):
 219     """Raised anywhere in the macro, caught outside the main loop."""
 220 
 221     def __init__(self, msg=''):
 222         """msg -- a string to be displayed to the user"""
 223         self.msg = msg
 224 
 225 
 226 def error(msg, args):
 227     """Return a message followed by usage information, all in HTML.
 228 
 229     msg  -- a string describing the error
 230     args -- the macro's original argument string
 231 
 232     """
 233     html = """
 234 <tt class="wiki">[[SortBy(%s)]]</tt><br>
 235 <strong class="error">SortBy: %s</strong><br>
 236 <small>%s<br>
 237 Valid sort types are: %s</small>"""
 238     msg = cgi.escape(msg)
 239     usage = cgi.escape("""Usage: SortBy(TablePage, <number of header rows>,
 240             <primary sort column number>, <sort type>
 241             [, <secondary sort column>, <sort type>] ... )""")
 242     sorts = ' '.join(valid_sorts)
 243     return html % (args, msg, usage, sorts)
 244 
 245 
 246 def read_table(page_file):
 247     """Read page_file and convert its first table's lines into row data"""
 248     file = open(page_file)
 249     intable = 0
 250     table_rows = []
 251     for line in file.xreadlines():
 252         if len(line) < 2 or line[0:2] != '||':
 253             if intable: break # We're done
 254             else: continue    # Skip non-table lines until we hit the table
 255         else:
 256             intable = 1
 257             table_rows.append(line[:-1]) # strip the \n while we're at it
 258     return table_rows
 259 
 260 
 261 def strip_fmt(arg):
 262     """Remove formatting characters and leading/trailing whitespace.
 263 
 264     Commas (such as in 1,000,000) are considered formatting chars."""
 265     for fmt_string in ["'''", "''", '{{{', '}}}', ',']:
 266         arg = arg.replace(fmt_string, '')
 267     return arg.strip()
 268 
 269 
 270 def to_number(arg):
 271     """Convert arg to int or float if possible, else return a string."""
 272     arg = strip_fmt(arg)
 273     try: return int(arg)
 274     except ValueError:
 275         try: return float(arg)
 276         except ValueError: return arg
 277 
 278 
 279 def to_nocase(arg):
 280     """Return arg in lowercase with no formatting characters."""
 281     return strip_fmt(arg).lower()
 282 
 283 
 284 def to_nosort(arg, count=[0]):
 285     """Return a higher integer each time so rows don't move when sorted."""
 286     count[0] += 1   # count is a default arg, so references the same list and
 287     return count[0] # incr's the same [0] every time the function is called.
 288 
 289 
 290 decorate_functions = {'alpha': strip_fmt,
 291                       'number': to_number,          
 292                       'nocase': to_nocase,
 293                       'nosort': to_nosort}
 294 valid_sorts = decorate_functions.keys()
 295 valid_sorts.sort()
 296 valid_sorts.extend(['reverse'+sort_name for sort_name in valid_sorts])
 297 
 298 
 299 def process_args(args, request):
 300     """Parse args string, return (sort_page, table, num_headers, sort_list)."""
 301     arglist = re.split(r'\s*,\s*', args.strip())
 302     if len(arglist) < 4:
 303         msg = """Not enough arguments (%s).
 304                  At least 4 are required.""" % len(arglist)
 305         raise SortByError, msg
 306     table_page, num_headers = arglist[0:2]
 307     try: num_headers = int(num_headers)
 308     except ValueError:
 309         msg = """Number of header rows (%s)
 310                  must be an integer""" % num_headers
 311         raise SortByError, msg
 312     if num_headers < 0:
 313         msg = """Number of header rows (%s)
 314                  must be a positive integer""" % num_headers
 315         raise SortByError, msg
 316     arglist[:2] = []  # Make arglist contain only column & sort_type pairs
 317     if len(arglist)%2 != 0:
 318         raise SortByError, 'Odd number of arguments (%s)' % (len(arglist)+2)
 319     sort_list = []
 320     while arglist:
 321 
 322         # Pop the sort_type and column from the end of the arglist.
 323         # They get stored in the sort_list in the opposite order
 324         # they were requested so the primary sort is done last.
 325 
 326         sort_type = arglist.pop().lower()
 327         sort_column = arglist.pop()
 328         try: sort_column = int(sort_column)
 329         except ValueError:
 330             msg = 'Column number (%s) must be an integer' % sort_column
 331             raise SortByError, msg
 332         if sort_column < 1:
 333             msg = """Column number (%s) must be 1 or higher.
 334                      1 = leftmost column""" % sort_column
 335             raise SortByError, msg
 336         sort_column -= 1  # Use zero-based column indexing internally
 337         if sort_type[:7] == 'reverse': # If the sort_type begins with 'reverse'
 338             reverse = 1                # set the reverse flag  
 339             sort_type = sort_type[7:]  # and strip 'reverse' from sort_type
 340         else: reverse = 0
 341         sort_list.append((sort_column, sort_type, reverse))  # Append a 3-tuple
 342     sort_page = Page(request, table_page)
 343     try: table_rows = read_table(sort_page._text_filename())
 344     except IOError:
 345         raise SortByError, 'Unable to open the table page "%s"' % table_page
 346     table = [row.split('||')[1:-1] for row in table_rows]
 347     if num_headers > len(table):
 348         msg = """Number of header rows (%s) is more than
 349                  the table length (%s)""" % (num_headers, len(table))
 350         raise SortByError, msg
 351     return (sort_page, table, num_headers, sort_list)
 352 
 353 
 354 def sort_table(table, num_headers, sort_column, sort_type, reverse):
 355     """Sort of the table (in-place), preserving num_headers rows at the top.
 356 
 357     Arguments:
 358     table       -- a list of lists representing rows of column entries
 359     num_headers -- an integer number of rows to keep at the top of the table
 360     sort_column -- the column number (zero-based) containing the sort values
 361     sort_type   -- which kind of sort to perform on the values being sorted
 362     reverse     -- 0 or 1, meaning an ascending or descending (reverse) sort
 363 
 364     """
 365     header = table[:num_headers]
 366     table[:num_headers] = []
 367 
 368     if table and sort_column > min([len(row) for row in table]):
 369         msg = """Column number (%s) is higher than
 370                  the length of one or more rows"""  % (sort_column+1)
 371         raise SortByError, msg
 372     if sort_type not in valid_sorts:
 373         raise SortByError, 'Invalid sort type "%s"' % sort_type
 374     decorate = decorate_functions[sort_type]
 375 
 376     # Use the 'decorate, sort, undecorate' pattern with ascending or
 377     # descending indices to ensure that the sort is stable.
 378 
 379     decorations = [decorate(row[sort_column]) for row in table]
 380     if reverse: indices = xrange(len(table), 0, -1)
 381     else: indices = xrange(len(table))
 382     decorated = zip(decorations, indices, table)
 383     decorated.sort()
 384     table[:] = [row for (decoration, index, row) in decorated]
 385 
 386     if reverse: table.reverse()
 387     table[:0] = header
 388     return
 389 
 390 
 391 def format(sort_page, macro, table):
 392     """Format the sorted table and return it as an HTML fragment.
 393 
 394     Arguments:
 395     sort_page -- a MoinMoin Page object representing the table page
 396     macro     -- the macro argument that was provided by MoinMoin
 397     table     -- a list of lists representing rows of column entries
 398 
 399     """
 400     ret = ''
 401     table_rows = ['||%s||' % '||'.join(row) for row in table]
 402     sort_page.set_raw_body('\n'.join(table_rows), 1)  # format the table
 403 
 404     # Here's some tricky stuff copied from Richard Jones' Include macro.
 405     stdout = sys.stdout
 406     sys.stdout = StringIO.StringIO()
 407     sort_page.send_page(content_only=1)  ## Removed macro.request argument for compatibility with Moin 1.7 (no longer needed).
 408     ret += sys.stdout.getvalue()
 409     sys.stdout = stdout
 410 
 411     # Output a helper link to get to the page with the table.
 412     name = sort_page.page_name
 413     ret += '<small>' + macro.formatter.url(1, name) + \
 414            macro.formatter.text('[goto %s]' % name) + macro.formatter.url(0) + \
 415            '</small>'
 416     return ret    
 417 
 418 
 419 def execute(macro, args):
 420     """Parse args, sort_table() repeatedly, return formatted table as HTML.
 421 
 422     Sort the table once for each column & sort_type in the sort_list
 423     beginning with the last (and least significant) sort in args
 424     because they were 'popped' from the end of the args. Since
 425     sort_table() uses a stable sort algorithm, the ordering of the
 426     earlier sorts is preserved in the later sorts when there are
 427     duplicates of the sort key.
 428 
 429     """
 430     try:
 431         sort_page, table, num_headers, sort_list = process_args(args, macro.request)
 432         for sort_column, sort_type, reverse in sort_list:
 433             sort_table(table, num_headers, sort_column, sort_type, reverse)
 434         return format(sort_page, macro, table)
 435     except SortByError, e: return error(e.msg, args)

Attached Files

To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.
  • [get | view] (2008-06-24 21:07:57, 14.7 KB) [[attachment:SortBy-1.7.py]]
  • [get | view] (2007-03-01 18:24:20, 14.5 KB) [[attachment:SortBy.py]]
 All files | Selected Files: delete move to page copy to page

You are not allowed to attach a file to this page.