# Copyright (C) University of Tennessee Health Science Center, Memphis, TN. # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License # as published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU Affero General Public License for more details. # # This program is available from Source Forge: at GeneNetwork Project # (sourceforge.net/projects/genenetwork/). # # Contact Drs. Robert W. Williams and Xiaodong Zhou (2010) # at rwilliams@uthsc.edu and xzhou15@uthsc.edu # # # # This module is used by GeneNetwork project (www.genenetwork.org) # # Created by GeneNetwork Core Team 2010/08/10 # # Last updated by GeneNetwork Core Team 2010/10/20 import string import time import re import math from math import * from htmlgen import HTMLgen2 as HT from base import webqtlConfig # NL, 07/27/2010. moved from webqtlForm.py #Dict of Parents and F1 information, In the order of [F1, Mat, Pat] ParInfo ={ 'BXH':['BHF1', 'HBF1', 'C57BL/6J', 'C3H/HeJ'], 'AKXD':['AKF1', 'KAF1', 'AKR/J', 'DBA/2J'], 'BXD':['B6D2F1', 'D2B6F1', 'C57BL/6J', 'DBA/2J'], 'BXD300':['B6D2F1', 'D2B6F1', 'C57BL/6J', 'DBA/2J'], 'B6BTBRF2':['B6BTBRF1', 'BTBRB6F1', 'C57BL/6J', 'BTBRT<+>tf/J'], 'BHHBF2':['B6HF2','HB6F2','C57BL/6J','C3H/HeJ'], 'BHF2':['B6HF2','HB6F2','C57BL/6J','C3H/HeJ'], 'B6D2F2':['B6D2F1', 'D2B6F1', 'C57BL/6J', 'DBA/2J'], 'BDF2-1999':['B6D2F2', 'D2B6F2', 'C57BL/6J', 'DBA/2J'], 'BDF2-2005':['B6D2F1', 'D2B6F1', 'C57BL/6J', 'DBA/2J'], 'CTB6F2':['CTB6F2','B6CTF2','C57BL/6J','Castaneous'], 'CXB':['CBF1', 'BCF1', 'C57BL/6ByJ', 'BALB/cByJ'], 'AXBXA':['ABF1', 'BAF1', 'C57BL/6J', 'A/J'], 'AXB':['ABF1', 'BAF1', 'C57BL/6J', 'A/J'], 'BXA':['BAF1', 'ABF1', 'C57BL/6J', 'A/J'], 'LXS':['LSF1', 'SLF1', 'ISS', 'ILS'], 'HXBBXH':['HSRBNF1', 'BNHSRF1', 'BN', 'HSR'], 'BayXSha':['BayXShaF1', 'ShaXBayF1', 'Bay-0','Shahdara'], 'ColXBur':['ColXBurF1', 'BurXColF1', 'Col-0','Bur-0'], 'ColXCvi':['ColXCviF1', 'CviXColF1', 'Col-0','Cvi'], 'SXM':['SMF1', 'MSF1', 'Steptoe','Morex'] } # NL, 07/27/2010. moved from template.py IMGSTEP1 = HT.Image('/images/step1.gif', alt='STEP 1',border=0) #XZ, Only be used in inputPage.py IMGSTEP2 = HT.Image('/images/step2.gif', alt='STEP 2',border=0) #XZ, Only be used in inputPage.py IMGSTEP3 = HT.Image('/images/step3.gif', alt='STEP 3',border=0) #XZ, Only be used in inputPage.py IMGNEXT = HT.Image('/images/arrowdown.gif', alt='NEXT',border=0) #XZ, Only be used in inputPage.py IMGASC = HT.Image("/images/sortup.gif", border=0) IMGASCON = HT.Image("/images/sortupon.gif", border=0) IMGDESC = HT.Image("/images/sortdown.gif", border=0) IMGDESCON = HT.Image("/images/sortdownon.gif", border=0) """ IMGASC = HT.Image("/images/sortup_icon.gif", border=0) IMGASCON = HT.Image("/images/sortupon.gif", border=0) IMGDESC = HT.Image("/images/sortdown_icon.gif", border=0) IMGDESCON = HT.Image("/images/sortdownon.gif", border=0) IMG_UNSORTED = HT.Image("/images/unsorted_icon.gif", border=0) """ PROGRESSBAR = HT.Image('/images/waitAnima2.gif', alt='checkblue',align="middle",border=0) ######################################### # Accessory Functions ######################################### def decodeEscape(str): a = str pattern = re.compile('(%[0-9A-Fa-f][0-9A-Fa-f])') match = pattern.findall(a) matched = [] for item in match: if item not in matched: a = a.replace(item, '%c' % eval("0x"+item[-2:])) matched.append(item) return a def exportData(hddn, tdata, NP = None): for key in tdata.keys(): _val, _var, _N = tdata[key].val, tdata[key].var, tdata[key].N if _val != None: hddn[key] = _val if _var != None: hddn['V'+key] = _var if NP and _N != None: hddn['N'+key] = _N def genShortStrainName(RISet='', input_strainName=''): #aliasStrainDict = {'C57BL/6J':'B6','DBA/2J':'D2'} strainName = input_strainName if RISet != 'AXBXA': if RISet == 'BXD300': this_RISet = 'BXD' elif RISet == 'BDF2-2005': this_RISet = 'CASE05_' else: this_RISet = RISet strainName = string.replace(strainName,this_RISet,'') strainName = string.replace(strainName,'CASE','') try: strainName = "%02d" % int(strainName) except: pass else: strainName = string.replace(strainName,'AXB','A') strainName = string.replace(strainName,'BXA','B') try: strainName = strainName[0] + "%02d" % int(strainName[1:]) except: pass return strainName def toInt(in_str): "Converts an arbitrary string to an unsigned integer" start = -1 end = -1 for i, char in enumerate(in_str): if char >= '0' and char <= '9': if start < 0: start = i end = i+1 else: if start >= 0: break if start < end: return int(in_str[start:end]) else: return -1 def transpose(m): 'transpose a matrix' n = len(m) return [[m[j][i] for i in range(len(m[0])) for j in range(n)][k*n:k*n+n] for k in range(len(m[0]))] def asymTranspose(m): 'transpose a matrix' t = max(map(len, m)) n = len(m) m2 = [["-"]]*n for i in range(n): m2[i] = m[i] + [""]*(t- len(m[i])) return [[m2[j][i] for i in range(len(m2[0])) for j in range(n)][k*n:k*n+n] for k in range(len(m2[0]))] def genRandStr(prefix = "", length=8, chars=string.letters+string.digits): from random import choice _str = prefix[:] for i in range(length): _str += choice(chars) return _str def generate_session(): import sha return sha.new(str(time.time())).hexdigest() def cvt2Dict(x): tmp = {} for key in x.keys(): tmp[key] = x[key] return tmp def dump_session(session_obj, filename): "It seems mod python can only cPickle most basic data type" import cPickle session_file = open(filename, 'wb') #try: # pass #except: # pass cPickle.dump(session_obj, session_file) session_file.close() def StringAsFloat(str): 'Converts string to float but catches any exception and returns None' try: return float(str) except: return None def IntAsFloat(str): 'Converts string to Int but catches any exception and returns None' try: return int(str) except: return None def FloatAsFloat(flt): 'Converts float to string but catches any exception and returns None' try: return float("%2.3f" % flt) except: return None def RemoveZero(flt): 'Converts string to float but catches any exception and returns None' try: if abs(flt) < 1e-6: return None else: return flt except: return None def SciFloat(d): 'Converts string to float but catches any exception and returns None' try: if abs(d) <= 1.0e-4: return "%1.2e" % d else: return "%1.5f" % d except: return None ###To be removed def FloatList2String(lst): 'Converts float list to string but catches any exception and returns None' tt='' try: for item in lst: if item == None: tt += 'X ' else: tt += '%f ' % item return tt except: return "" def ListNotNull(lst): '''Obsolete - Use built in function any (or all or whatever) Determine if the elements in a list are all null ''' for item in lst: if item is not None: return 1 return None ###To be removed def FileDataProcess(str): 'Remove the description text from the input file if theres any' i=0 while i<len(str): if str[i]<'\x7f' and str[i]>'\x20': break else: i+=1 str=str[i:] str=string.join(string.split(str,'\000'),'') i=string.find(str,"*****") if i>-1: return str[i+5:] else: return str def rank(a,lst,offset=0): """Calculate the integer rank of a number in an array, can be used to calculate p-value""" n = len(lst) if n == 2: if a <lst[0]: return offset elif a > lst[1]: return offset + 2 else: return offset +1 elif n == 1: if a <lst[0]: return offset else: return offset +1 elif n== 0: return offset else: mid = n/2 if a < lst[mid]: return rank(a,lst[:mid-1],offset) else: return rank(a,lst[mid:],offset+mid) def cmpScanResult(A,B): try: if A.LRS > B.LRS: return 1 elif A.LRS == B.LRS: return 0 else: return -1 except: return 0 def cmpScanResult2(A,B): try: if A.LRS < B.LRS: return 1 elif A.LRS == B.LRS: return 0 else: return -1 except: return 0 def cmpOrder(A,B): try: if A[1] < B[1]: return -1 elif A[1] == B[1]: return 0 else: return 1 except: return 0 def cmpOrder2(A,B): try: if A[-1] < B[-1]: return -1 elif A[-1] == B[-1]: return 0 else: return 1 except: return 0 def calRank(xVals, yVals, N): ### Zach Sloan, February 4 2010 """ Returns a ranked set of X and Y values. These are used when generating a Spearman scatterplot. Bear in mind that this sets values equal to each other as the same rank. """ XX = [] YY = [] X = [0]*len(xVals) Y = [0]*len(yVals) j = 0 for i in range(len(xVals)): if xVals[i] != None and yVals[i] != None: XX.append((j, xVals[i])) YY.append((j, yVals[i])) j = j + 1 NN = len(XX) XX.sort(cmpOrder2) YY.sort(cmpOrder2) j = 1 rank = 0.0 while j < NN: if XX[j][1] != XX[j-1][1]: X[XX[j-1][0]] = j j = j+1 else: jt = j+1 ji = j for jt in range(j+1, NN): if (XX[jt][1] != XX[j-1][1]): break rank = 0.5*(j+jt) for ji in range(j-1, jt): X[XX[ji][0]] = rank if (jt == NN-1): if (XX[jt][1] == XX[j-1][1]): X[XX[NN-1][0]] = rank j = jt+1 if j == NN: if X[XX[NN-1][0]] == 0: X[XX[NN-1][0]] = NN j = 1 rank = 0.0 while j < NN: if YY[j][1] != YY[j-1][1]: Y[YY[j-1][0]] = j j = j+1 else: jt = j+1 ji = j for jt in range(j+1, NN): if (YY[jt][1] != YY[j-1][1]): break rank = 0.5*(j+jt) for ji in range(j-1, jt): Y[YY[ji][0]] = rank if (jt == NN-1): if (YY[jt][1] == YY[j-1][1]): Y[YY[NN-1][0]] = rank j = jt+1 if j == NN: if Y[YY[NN-1][0]] == 0: Y[YY[NN-1][0]] = NN return (X,Y) def calCorrelationRank(xVals,yVals,N): """ Calculated Spearman Ranked Correlation. The algorithm works by setting all tied ranks to the average of those ranks (for example, if ranks 5-10 all have the same value, each will be set to rank 7.5). """ XX = [] YY = [] j = 0 for i in range(len(xVals)): if xVals[i]!= None and yVals[i]!= None: XX.append((j,xVals[i])) YY.append((j,yVals[i])) j = j+1 NN = len(XX) if NN <6: return (0.0,NN) XX.sort(cmpOrder2) YY.sort(cmpOrder2) X = [0]*NN Y = [0]*NN j = 1 rank = 0.0 t = 0.0 sx = 0.0 while j < NN: if XX[j][1] != XX[j-1][1]: X[XX[j-1][0]] = j j = j+1 else: jt = j+1 ji = j for jt in range(j+1, NN): if (XX[jt][1] != XX[j-1][1]): break rank = 0.5*(j+jt) for ji in range(j-1, jt): X[XX[ji][0]] = rank t = jt-j sx = sx + (t*t*t-t) if (jt == NN-1): if (XX[jt][1] == XX[j-1][1]): X[XX[NN-1][0]] = rank j = jt+1 if j == NN: if X[XX[NN-1][0]] == 0: X[XX[NN-1][0]] = NN j = 1 rank = 0.0 t = 0.0 sy = 0.0 while j < NN: if YY[j][1] != YY[j-1][1]: Y[YY[j-1][0]] = j j = j+1 else: jt = j+1 ji = j for jt in range(j+1, NN): if (YY[jt][1] != YY[j-1][1]): break rank = 0.5*(j+jt) for ji in range(j-1, jt): Y[YY[ji][0]] = rank t = jt - j sy = sy + (t*t*t-t) if (jt == NN-1): if (YY[jt][1] == YY[j-1][1]): Y[YY[NN-1][0]] = rank j = jt+1 if j == NN: if Y[YY[NN-1][0]] == 0: Y[YY[NN-1][0]] = NN D = 0.0 for i in range(NN): D += (X[i]-Y[i])*(X[i]-Y[i]) fac = (1.0 -sx/(NN*NN*NN-NN))*(1.0-sy/(NN*NN*NN-NN)) return ((1-(6.0/(NN*NN*NN-NN))*(D+(sx+sy)/12.0))/math.sqrt(fac),NN) def calCorrelationRankText(dbdata,userdata,N): ### dcrowell = David Crowell, July 2008 """Calculates correlation ranks with data formatted from the text file. dbdata, userdata are lists of strings. N is an int. Returns a float. Used by correlationPage""" XX = [] YY = [] j = 0 for i in range(N): if (dbdata[i]!= None and userdata[i]!=None) and (dbdata[i]!= 'None' and userdata[i]!='None'): XX.append((j,float(dbdata[i]))) YY.append((j,float(userdata[i]))) j += 1 NN = len(XX) if NN <6: return (0.0,NN) XX.sort(cmpOrder2) YY.sort(cmpOrder2) X = [0]*NN Y = [0]*NN j = 1 rank = 0.0 t = 0.0 sx = 0.0 while j < NN: if XX[j][1] != XX[j-1][1]: X[XX[j-1][0]] = j j = j+1 else: jt = j+1 ji = j for jt in range(j+1, NN): if (XX[jt][1] != XX[j-1][1]): break rank = 0.5*(j+jt) for ji in range(j-1, jt): X[XX[ji][0]] = rank t = jt-j sx = sx + (t*t*t-t) if (jt == NN-1): if (XX[jt][1] == XX[j-1][1]): X[XX[NN-1][0]] = rank j = jt+1 if j == NN: if X[XX[NN-1][0]] == 0: X[XX[NN-1][0]] = NN j = 1 rank = 0.0 t = 0.0 sy = 0.0 while j < NN: if YY[j][1] != YY[j-1][1]: Y[YY[j-1][0]] = j j = j+1 else: jt = j+1 ji = j for jt in range(j+1, NN): if (YY[jt][1] != YY[j-1][1]): break rank = 0.5*(j+jt) for ji in range(j-1, jt): Y[YY[ji][0]] = rank t = jt - j sy = sy + (t*t*t-t) if (jt == NN-1): if (YY[jt][1] == YY[j-1][1]): Y[YY[NN-1][0]] = rank j = jt+1 if j == NN: if Y[YY[NN-1][0]] == 0: Y[YY[NN-1][0]] = NN D = 0.0 for i in range(NN): D += (X[i]-Y[i])*(X[i]-Y[i]) fac = (1.0 -sx/(NN*NN*NN-NN))*(1.0-sy/(NN*NN*NN-NN)) return ((1-(6.0/(NN*NN*NN-NN))*(D+(sx+sy)/12.0))/math.sqrt(fac),NN) def calCorrelation(dbdata,userdata,N): X = [] Y = [] for i in range(N): if dbdata[i]!= None and userdata[i]!= None: X.append(dbdata[i]) Y.append(userdata[i]) NN = len(X) if NN <6: return (0.0,NN) sx = reduce(lambda x,y:x+y,X,0.0) sy = reduce(lambda x,y:x+y,Y,0.0) meanx = sx/NN meany = sy/NN xyd = 0.0 sxd = 0.0 syd = 0.0 for i in range(NN): xyd += (X[i] - meanx)*(Y[i]-meany) sxd += (X[i] - meanx)*(X[i] - meanx) syd += (Y[i] - meany)*(Y[i] - meany) try: corr = xyd/(sqrt(sxd)*sqrt(syd)) except: corr = 0 return (corr,NN) def calCorrelationText(dbdata,userdata,N): ### dcrowell July 2008 """Calculates correlation coefficients with values formatted from text files. dbdata, userdata are lists of strings. N is an int. Returns a float Used by correlationPage""" X = [] Y = [] for i in range(N): #if (dbdata[i]!= None and userdata[i]!= None) and (dbdata[i]!= 'None' and userdata[i]!= 'None'): # X.append(float(dbdata[i])) # Y.append(float(userdata[i])) if dbdata[i] == None or dbdata[i] == 'None' or userdata[i] == None or userdata[i] == 'None': continue else: X.append(float(dbdata[i])) Y.append(float(userdata[i])) NN = len(X) if NN <6: return (0.0,NN) sx = sum(X) sy = sum(Y) meanx = sx/float(NN) meany = sy/float(NN) xyd = 0.0 sxd = 0.0 syd = 0.0 for i in range(NN): x1 = X[i]-meanx y1 = Y[i]-meany xyd += x1*y1 sxd += x1**2 syd += y1**2 try: corr = xyd/(sqrt(sxd)*sqrt(syd)) except: corr = 0 return (corr,NN) def readLineCSV(line): ### dcrowell July 2008 """Parses a CSV string of text and returns a list containing each element as a string. Used by correlationPage""" returnList = line.split('","') returnList[-1]=returnList[-1][:-2] returnList[0]=returnList[0][1:] return returnList def cmpCorr(A,B): try: if abs(A[1]) < abs(B[1]): return 1 elif abs(A[1]) == abs(B[1]): return 0 else: return -1 except: return 0 def cmpLitCorr(A,B): try: if abs(A[3]) < abs(B[3]): return 1 elif abs(A[3]) == abs(B[3]): if abs(A[1]) < abs(B[1]): return 1 elif abs(A[1]) == abs(B[1]): return 0 else: return -1 else: return -1 except: return 0 def cmpPValue(A,B): try: if A.corrPValue < B.corrPValue: return -1 elif A.corrPValue == B.corrPValue: if abs(A.corr) > abs(B.corr): return -1 elif abs(A.corr) < abs(B.corr): return 1 else: return 0 else: return 1 except: return 0 def cmpEigenValue(A,B): try: if A[0] > B[0]: return -1 elif A[0] == B[0]: return 0 else: return 1 except: return 0 def cmpLRSFull(A,B): try: if A[0] < B[0]: return -1 elif A[0] == B[0]: return 0 else: return 1 except: return 0 def cmpLRSInteract(A,B): try: if A[1] < B[1]: return -1 elif A[1] == B[1]: return 0 else: return 1 except: return 0 def cmpPos(A,B): try: try: AChr = int(A.chr) except: AChr = 20 try: BChr = int(B.chr) except: BChr = 20 if AChr > BChr: return 1 elif AChr == BChr: if A.mb > B.mb: return 1 if A.mb == B.mb: return 0 else: return -1 else: return -1 except: return 0 def cmpGenoPos(A,B): try: A1 = A.chr B1 = B.chr try: A1 = int(A1) except: A1 = 25 try: B1 = int(B1) except: B1 = 25 if A1 > B1: return 1 elif A1 == B1: if A.mb > B.mb: return 1 if A.mb == B.mb: return 0 else: return -1 else: return -1 except: return 0 #XZhou: Must use "BINARY" to enable case sensitive comparison. def authUser(name,password,db, encrypt=None): try: if encrypt: query = 'SELECT privilege, id,name,password, grpName FROM User WHERE name= BINARY \'%s\' and password= BINARY \'%s\'' % (name,password) else: query = 'SELECT privilege, id,name,password, grpName FROM User WHERE name= BINARY \'%s\' and password= BINARY SHA(\'%s\')' % (name,password) db.execute(query) records = db.fetchone() if not records: raise ValueError return records#(privilege,id,name,password,grpName) except: return (None, None, None, None, None) def hasAccessToConfidentialPhenotypeTrait(privilege, userName, authorized_users): access_to_confidential_phenotype_trait = 0 if webqtlConfig.USERDICT[privilege] > webqtlConfig.USERDICT['user']: access_to_confidential_phenotype_trait = 1 else: AuthorizedUsersList=map(string.strip, string.split(authorized_users, ',')) if AuthorizedUsersList.__contains__(userName): access_to_confidential_phenotype_trait = 1 return access_to_confidential_phenotype_trait class VisualizeException(Exception): def __init__(self, message): self.message = message def __str__(self): return self.message # safeConvert : (string -> A) -> A -> A # to convert a string to type A, using the supplied default value # if the given conversion function doesn't work def safeConvert(f, value, default): try: return f(value) except: return default # safeFloat : string -> float -> float # to convert a string to a float safely def safeFloat(value, default): return safeConvert(float, value, default) # safeInt: string -> int -> int # to convert a string to an int safely def safeInt(value, default): return safeConvert(int, value, default) # safeString : string -> (arrayof string) -> string -> string # if a string is not in a list of strings to pick a default value # for that string def safeString(value, validChoices, default): if value in validChoices: return value else: return default # yesNoToInt: string -> int # map "yes" -> 1 and "no" -> 0 def yesNoToInt(value): if value == "yes": return 1 elif value == "no": return 0 else: return None # IntToYesNo: int -> string # map 1 -> "yes" and 0 -> "no" def intToYesNo(value): if value == 1: return "yes" elif value == 0: return "no" else: return None def formatField(name): name = name.replace("_", " ") name = name.title() #name = name.replace("Mb Mm6", "Mb"); return name.replace("Id", "ID") #XZ, 03/27/2009: This function is very specific. #It is used by AJAX_table.py, correlationPage.py and dataPage.py def genTableObj(tblobj=None, file="", sortby = ("", ""), tableID = "sortable", addIndex = "1", hiddenColumns=[]): header = tblobj['header'] body = tblobj['body'] field, order = sortby #ZAS 9/12/2011 - The hiddenColumns array needs to be converted into a string so they can be placed into the javascript of each up/down button hiddenColumnsString = ",".join(hiddenColumns) tbl = HT.TableLite(Class="collap b2", cellspacing=1, cellpadding=5) hiddenColumnIdx = [] #indices of columns to hide idx = -1 last_idx = 0 #ZS: This is the index of the last item in the regular table header (without any extra parameters). It is used to determine the index of each extra parameter. for row in header: hr = HT.TR() for i, item in enumerate(row): if (item.text == '') or (item.text not in hiddenColumns): if item.sort and item.text: down = HT.Href("javascript:xmlhttpPost('%smain.py?FormID=AJAX_table', '%s', 'sort=%s&order=down&file=%s&tableID=%s&addIndex=%s&hiddenColumns=%s')" % (webqtlConfig.CGIDIR, tableID, item.text, file, tableID, addIndex, hiddenColumnsString),IMGDESC) up = HT.Href("javascript:xmlhttpPost('%smain.py?FormID=AJAX_table', '%s', 'sort=%s&order=up&file=%s&tableID=%s&addIndex=%s&hiddenColumns=%s')" % (webqtlConfig.CGIDIR, tableID, item.text, file, tableID, addIndex, hiddenColumnsString),IMGASC) if item.text == field: idx = item.idx last_idx = idx if order == 'up': up = IMGASCON elif order == 'down': down = IMGDESCON item.html.append(HT.Div(up, down, style="float: bottom;")) hr.append(item.html) else: hiddenColumnIdx.append(i) tbl.append(hr) for i, row in enumerate(body): for j, item in enumerate(row): if order == 'down': if (item.val == '' or item.val == 'x' or item.val == 'None'): item.val = 0 if order == 'up': if (item.val == '' or item.val == 'x' or item.val == 'None'): item.val = 'zzzzz' if idx >= 0: if order == 'down': body.sort(lambda A, B: cmp(B[idx].val, A[idx].val), key=natsort_key) elif order == 'up': body.sort(lambda A, B: cmp(A[idx].val, B[idx].val), key=natsort_key) else: pass for i, row in enumerate(body): hr = HT.TR(Id = row[0].text) for j, item in enumerate(row): if (j not in hiddenColumnIdx): if j == 0: if addIndex == "1": item.html.contents = [i+1] + item.html.contents hr.append(item.html) tbl.append(hr) return tbl def natsort_key(string): r = [] for c in string: try: c = int(c) try: r[-1] = r[-1] * 10 + c except: r.append(c) except: r.append(c) return r