Update: Sorting Target XML Node Sets In ColdFusion XML Documents

<cffunction
	name="XmlSort2"
	access="public"
	returntype="xml"
	output="true"
	hint="I sort part of an XML documument based on the given XPath and sort characteristics.">
 
	<!--- Define arguments. --->
	<cfargument
		name="Xml"
		type="any"
		required="true"
		hint="I am an XML string or ColdFusion XML document."
		/>
 
	<cfargument
		name="XPath"
		type="string"
		required="true"
		hint="I am the XPath to the target node set." />
 
	<cfargument
		name="SortXPath"
		type="any"
		required="false"
		default="text()"
		hint="I am the XPath value upon which the sort is being conducted. This can be a string or an array (if multiple sorting options are required)."
		/>
 
	<cfargument
		name="Direction"
		type="string"
		required="false"
		default="ASC"
		hint="I am the sort direction. ASC or DESC."
		/>
 
	<!--- Define the local scope. --->
	<cfset var LOCAL = {} />
 
 
	<!---
		Check to see if our XML document is an Xml document.
		If is not, then we want to convert it into a true
		ColdFusion XML document.
	--->
	<cfif NOT IsXmlDoc( ARGUMENTS.Xml )>
 
		<!--- Convert to an XML document. --->
		<cfset ARGUMENTS.Xml = XmlParse(
			Trim( ARGUMENTS.Xml )
			) />
 
	</cfif>
 
 
	<!---
		Check to see if the given sorting option is a string or
		an array. If it's a string, then let's convert it to an
		array so that we can treat it uniformily later on.
	--->
	<cfif IsSimpleValue( ARGUMENTS.SortXPath )>
 
		<!---
			We need to copy this to get around a bug in the way
			ColdFusion handles implicit array creation involving
			its own values.
		--->
		<cfset LOCAL.SortCopy = ARGUMENTS.SortXPath />
 
		<!--- Convert simple value to an array. --->
		<cfset ARGUMENTS.SortXPath = [ LOCAL.SortCopy ] />
 
	</cfif>
 
 
	<!--- Get the set of target nodes. --->
	<cfset LOCAL.TargetNodes = XmlSearch(
		ARGUMENTS.Xml,
		ARGUMENTS.XPath
		) />
 
 
	<!---
		Check to make sure we have target nodes. If not,
		then just return the original XML.
	--->
	<cfif (ArrayLen( LOCAL.TargetNodes ) LT 2)>
 
		<cfreturn ARGUMENTS.Xml />
 
	</cfif>
 
 
	<!---
		ASSERT: At this point, we know that we have a valid
		set of target nodes that can be sorted.
	--->
 
 
	<!--- Perform bubble sort on target nodes. --->
	<cfloop
		index="LOCAL.EndIndex"
		from="#(ArrayLen( LOCAL.TargetNodes ) - 1)#"
		to="1"
		step="-1">
 
		<!---
			Loop over nodes starting at 1 and then proceeding
			until we reach the "end index". This way, we will
			not duplicate our comparison of the last value.
		--->
		<cfloop
			index="LOCAL.Index"
			from="1"
			to="#LOCAL.EndIndex#"
			step="1">
 
			<!--- Get the two target nodes. --->
			<cfset LOCAL.NodeOne = LOCAL.TargetNodes[ LOCAL.Index ] />
			<cfset LOCAL.NodeTwo = LOCAL.TargetNodes[ LOCAL.Index + 1 ] />
 
 
			<!---
				Now that we have the two target nodes, we have
				to sort them based on the array of comparison
				values. We only need to proceed through the
				comparisons if the previous comparison is equal.
			--->
			<cfloop
				index="LOCAL.SortXPath"
				array="#ARGUMENTS.SortXPath#">
 
				<!--- Get the comparison value for given node. --->
				<cfset LOCAL.ValueOne = XmlSearch(
					LOCAL.NodeOne,
					LOCAL.SortXPath
					) />
 
				<!--- Get the comparison value for next node. --->
				<cfset LOCAL.ValueTwo = XmlSearch(
					LOCAL.NodeTwo,
					LOCAL.SortXPath
					) />
 
 
				<!---
					Now, we have to get our values down to
					something that is usable. If this is a node,
					then get the text value. If this is an
					attribute then get the attribute value.
				--->
				<cfif StructKeyExists(
					LOCAL.ValueOne[ 1 ],
					"XmlText"
					)>
 
					<!--- Get node text. --->
					<cfset LOCAL.ValueOne = LOCAL.ValueOne[ 1 ].XmlText />
					<cfset LOCAL.ValueTwo = LOCAL.ValueTwo[ 1 ].XmlText />
 
				<cfelse>
 
					<!--- Get attribute value. --->
					<cfset LOCAL.ValueOne = LOCAL.ValueOne[ 1 ].XmlValue />
					<cfset LOCAL.ValueTwo = LOCAL.ValueTwo[ 1 ].XmlValue />
 
				</cfif>
 
 
				<!---
					Check to see if these two values are equal.
					If they are, then we just want to proceed
					to the next sorting condition. It is only if
					they are unequal that we can take action.
				--->
				<cfif (LOCAL.ValueOne NEQ LOCAL.ValueTwo)>
 
					<!---
						Check to see which direction we are
						sorting in.
					--->
					<cfif (
						(
							(ARGUMENTS.Direction EQ "ASC") AND
							(LOCAL.ValueOne GT LOCAL.ValueTwo)
						) OR
						(
							(ARGUMENTS.Direction EQ "DESC") AND
							(LOCAL.ValueOne LT LOCAL.ValueTwo)
						))>
 
						<!--- Swap nodes. --->
						<cfset ArraySwap(
							LOCAL.TargetNodes,
							LOCAL.Index,
							(LOCAL.Index + 1)
							) />
 
					</cfif>
 
 
					<!---
						Break out of the comparison sub-loop
						since we found two values which are
						not equal.
					--->
					<cfbreak />
 
				</cfif>
 
			</cfloop>
 
 
		</cfloop>
		<!--- END: Comparison loop. --->
 
	</cfloop>
	<!--- END: Outer loop. --->
 
 
	<!---
		ASSERT: At this point our disconnected set of target nodes
		is in the appropriate sort order.
	--->
 
 
	<!---
		Before we do anything, we need to set up a unique ID for
		each XML node. This way, as we go through and compare
		them, we can make unique comparisons. This is utilitarian
		and will have to be removed afterwards.
	--->
	<cfloop
		index="LOCAL.TargetNode"
		array="#LOCAL.TargetNodes#">
 
		<!---
			Set unique ID. Use a function name space to make
			sure we don't have have and attribute conflicts.
		--->
		<cfset LOCAL.TargetNode.XmlAttributes[ "XmlSort:UUID" ] = CreateUUID() />
 
	</cfloop>
 
 
	<!---
		Now, it's time to take that set of nodes and apply the
		order to the XML document. Because we don't know the
		placement within the existing document, we need to
		iterate over the parent node and check for matches.
	--->
	<cfset LOCAL.ParentNode = XmlSearch(
		LOCAL.TargetNodes[ 1 ],
		".."
		) />
 
	<!--- Get the parent node from the returned node set. --->
	<cfset LOCAL.ParentNode = LOCAL.ParentNode[ 1 ] />
 
	<!---
		As we replace our children, we need to keep a pointer
		to the index of the child we are replacing INTO the
		existing document. As we go along, this will help us
		to keep track since we don't know what order the matching
		nodes are going to show up in.
	--->
	<cfset LOCAL.ReplaceIndex = 1 />
 
 
	<!--- Loop over the xml children. --->
	<cfloop
		index="LOCAL.ChildIndex"
		from="1"
		to="#ArrayLen( LOCAL.ParentNode.XmlChildren )#"
		step="1">
 
		<!---
			Check to see if this node is one of the nodes in
			our target node set.
		--->
		<cfloop
			index="LOCAL.TargetNode"
			array="#LOCAL.TargetNodes#">
 
 
			<!---
				Check to see if this target node is the child
				node that we are currently examining. When
				comparing, make sure that the child node actually
				has the sorting UUID.
			--->
			<cfif (
				StructKeyExists( LOCAL.ParentNode.XmlChildren[ LOCAL.ChildIndex ].XmlAttributes, "XmlSort:UUID" ) AND
				(LOCAL.ParentNode.XmlChildren[ LOCAL.ChildIndex ].XmlAttributes[ "XmlSort:UUID" ] EQ LOCAL.TargetNode.XmlAttributes[ "XmlSort:UUID" ])
				)>
 
				<!---
					The current child IS in our target node set.
					That means that we can replace in one of our
					target nodes in this child index. Use the
					current index of replacement to select the
					target node.
 
					Use the post-incrementer to make sure that the
					replce index is upped after we copy over the
					taret node.
 
					Use Duplicate() so that we don't mess up our
					node references and accidentally delete one
					of the nodes from the parent document.
				--->
				<cfset LOCAL.ParentNode.XmlChildren[ LOCAL.ChildIndex ] = Duplicate( LOCAL.TargetNodes[ LOCAL.ReplaceIndex++ ] ) />
 
				<!--- Remove the UUID. --->
				<cfset StructDelete(
					LOCAL.ParentNode.XmlChildren[ LOCAL.ChildIndex ].XmlAttributes,
					"XmlSort:UUID"
					) />
 
				<!---
					We matched a node - no need to keep checking
					for a match in our taret node set (there
					should be a one-to-one match).
				--->
				<cfbreak />
 
			</cfif>
 
		</cfloop>
 
	</cfloop>
 
 
	<!--- Return the updated XML document. --->
	<cfreturn ARGUMENTS.Xml />
</cffunction>

For Cut-and-Paste