docs: function _fnSort ( oSettings, bApplyClasses )
  		{
  			var
  				i, iLen, j, jLen, k, kLen,
  				sDataType, nTh,
  				aaSort = [],
  			 	aiOrig = [],
  				oSort = DataTable.ext.oSort,
  				aoData = oSettings.aoData,
  				aoColumns = oSettings.aoColumns,
  				oAria = oSettings.oLanguage.oAria;
  			
  			/* No sorting required if server-side or no sorting array */
  			if ( !oSettings.oFeatures.bServerSide && 
  				(oSettings.aaSorting.length !== 0 || oSettings.aaSortingFixed !== null) )
  			{
  				aaSort = ( oSettings.aaSortingFixed !== null ) ?
  					oSettings.aaSortingFixed.concat( oSettings.aaSorting ) :
  					oSettings.aaSorting.slice();
  				
  				/* If there is a sorting data type, and a fuction belonging to it, then we need to
  				 * get the data from the developer's function and apply it for this column
  				 */
  				for ( i=0 ; i<aaSort.length ; i++ )
  				{
  					var iColumn = aaSort[i][0];
  					var iVisColumn = _fnColumnIndexToVisible( oSettings, iColumn );
  					sDataType = oSettings.aoColumns[ iColumn ].sSortDataType;
  					if ( DataTable.ext.afnSortData[sDataType] )
  					{
  						var aData = DataTable.ext.afnSortData[sDataType].call( 
  							oSettings.oInstance, oSettings, iColumn, iVisColumn
  						);
  						if ( aData.length === aoData.length )
  						{
  							for ( j=0, jLen=aoData.length ; j<jLen ; j++ )
  							{
  								_fnSetCellData( oSettings, j, iColumn, aData[j] );
  							}
  						}
  						else
  						{
  							_fnLog( oSettings, 0, "Returned data sort array (col "+iColumn+") is the wrong length" );
  						}
  					}
  				}
  				
  				/* Create a value - key array of the current row positions such that we can use their
  				 * current position during the sort, if values match, in order to perform stable sorting
  				 */
  				for ( i=0, iLen=oSettings.aiDisplayMaster.length ; i<iLen ; i++ )
  				{
  					aiOrig[ oSettings.aiDisplayMaster[i] ] = i;
  				}
  		
  				/* Build an internal data array which is specific to the sort, so we can get and prep
  				 * the data to be sorted only once, rather than needing to do it every time the sorting
  				 * function runs. This make the sorting function a very simple comparison
  				 */
  				var iSortLen = aaSort.length;
  				var fnSortFormat, aDataSort;
  				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
  				{
  					for ( j=0 ; j<iSortLen ; j++ )
  					{
  						aDataSort = aoColumns[ aaSort[j][0] ].aDataSort;
  		
  						for ( k=0, kLen=aDataSort.length ; k<kLen ; k++ )
  						{
  							sDataType = aoColumns[ aDataSort[k] ].sType;
  							fnSortFormat = oSort[ (sDataType ? sDataType : 'string')+"-pre" ];
  							
  							aoData[i]._aSortData[ aDataSort[k] ] = fnSortFormat ?
  								fnSortFormat( _fnGetCellData( oSettings, i, aDataSort[k], 'sort' ) ) :
  								_fnGetCellData( oSettings, i, aDataSort[k], 'sort' );
  						}
  					}
  				}
  				
  				/* Do the sort - here we want multi-column sorting based on a given data source (column)
  				 * and sorting function (from oSort) in a certain direction. It's reasonably complex to
  				 * follow on it's own, but this is what we want (example two column sorting):
  				 *  fnLocalSorting = function(a,b){
  				 *  	var iTest;
  				 *  	iTest = oSort['string-asc']('data11', 'data12');
  				 *  	if (iTest !== 0)
  				 *  		return iTest;
  				 *    iTest = oSort['numeric-desc']('data21', 'data22');
  				 *    if (iTest !== 0)
  				 *  		return iTest;
  				 *  	return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
  				 *  }
  				 * Basically we have a test for each sorting column, if the data in that column is equal,
  				 * test the next column. If all columns match, then we use a numeric sort on the row 
  				 * positions in the original data array to provide a stable sort.
  				 */
  				oSettings.aiDisplayMaster.sort( function ( a, b ) {
  					var k, l, lLen, iTest, aDataSort, sDataType;
  					for ( k=0 ; k<iSortLen ; k++ )
  					{
  						aDataSort = aoColumns[ aaSort[k][0] ].aDataSort;
  		
  						for ( l=0, lLen=aDataSort.length ; l<lLen ; l++ )
  						{
  							sDataType = aoColumns[ aDataSort[l] ].sType;
  							
  							iTest = oSort[ (sDataType ? sDataType : 'string')+"-"+aaSort[k][1] ](
  								aoData[a]._aSortData[ aDataSort[l] ],
  								aoData[b]._aSortData[ aDataSort[l] ]
  							);
  						
  							if ( iTest !== 0 )
  							{
  								return iTest;
  							}
  						}
  					}
  					
  					return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
  				} );
  			}
  			
  			/* Alter the sorting classes to take account of the changes */
  			if ( (bApplyClasses === undefined || bApplyClasses) && !oSettings.oFeatures.bDeferRender )
  			{
  				_fnSortingClasses( oSettings );
  			}
  		
  			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
  			{
  				var sTitle = aoColumns[i].sTitle.replace( /<.*?>/g, "" );
  				nTh = aoColumns[i].nTh;
  				nTh.removeAttribute('aria-sort');
  				nTh.removeAttribute('aria-label');
  				
  				/* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */
  				if ( aoColumns[i].bSortable )
  				{
  					if ( aaSort.length > 0 && aaSort[0][0] == i )
  					{
  						nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" );
  						
  						var nextSort = (aoColumns[i].asSorting[ aaSort[0][2]+1 ]) ? 
  							aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0];
  						nTh.setAttribute('aria-label', sTitle+
  							(nextSort=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
  					}
  					else
  					{
  						nTh.setAttribute('aria-label', sTitle+
  							(aoColumns[i].asSorting[0]=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
  					}
  				}
  				else
  				{
  					nTh.setAttribute('aria-label', sTitle);
  				}
  			}
  			
  			/* Tell the draw function that we have sorted the data */
  			oSettings.bSorted = true;
  			$(oSettings.oInstance).trigger('sort', oSettings);
  			
  			/* Copy the master data into the draw array and re-draw */
  			if ( oSettings.oFeatures.bFilter )
  			{
  				/* _fnFilter() will redraw the table for us */
  				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
  			}
  			else
  			{
  				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
  				oSettings._iDisplayStart = 0; /* reset display back to page 0 */
  				_fnCalculateEnd( oSettings );
  				_fnDraw( oSettings );
  			}
  		}