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