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

MoinMoin: macro/SortBy.py (last edited 2007-10-29 19:15:20 by localhost)