简体   繁体   中英

Sort XML Nodes by Alpha.Numeric using C#

Say I have an XmlDocument that I generate that has InnerXml that looks like this:

<ORM_O01>
  <MSH>
    <MSH.9>
      <MSG.2>O01</MSG.2>
    </MSH.9>
    <MSH.6>
      <HD.1>13702</HD.1>
    </MSH.6>
  </MSH>
  <ORM_O01.PATIENT>
   <PID>      
     <PID.18>
       <CX.1>SecondTestFin</CX.1>
     </PID.18>
     <PID.3>
        <CX.1>108</CX.1>
     </PID.3>
   </PID>
  </ORM_O01.PATIENT>
</ORM_O01>

As you can see node <PID.18> is before node <PID.3> . ( <MSH.9> is also before <MSH.6> .)

Restructuring my generation would cause my nice clean code to become very messy.

Is there a way to sort the nodes so that it will sort alpha until it hits the last period then sort numeric (if the last values are numbers)?

By "numeric sorting" I mean it will look at the whole number rather than char by char. (So 18 > 3).

The obvious answer is yes.

If this is the result you want:

<ORM_O01>
  <MSH>
    <MSH.6>
      <HD.1>13702</HD.1>
    </MSH.6>
    <MSH.9>
      <MSG.2>O01</MSG.2>
    </MSH.9>
  </MSH>
  <ORM_O01.PATIENT>
    <PID>
      <PID.3>
        <CX.1>108</CX.1>
      </PID.3>
      <PID.18>
        <CX.1>SecondTestFin</CX.1>
      </PID.18>
    </PID>
  </ORM_O01.PATIENT>
</ORM_O01>

Then this class will do it: (I should get paid for this...)

using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;

namespace Test
{
    public class SortXmlFile
    {
        XElement rootNode;

        public SortXmlFile(FileInfo file)
        {
            if (file.Exists)
                rootNode = XElement.Load(file.FullName);
            else
                throw new FileNotFoundException(file.FullName);
        }

        public XElement SortFile()
        {
            SortElements(rootNode);
            return rootNode;
        }

        public void SortElements(XElement root)
        {
            bool sortWithNumeric = false;
            XElement[] children = root.Elements().ToArray();
            foreach (XElement child in children)
            {
                string name;
                int value;
                // does any child need to be sorted by numeric?
                if (!sortWithNumeric && Sortable(child, out name, out value))
                    sortWithNumeric = true;
                child.Remove(); // we'll re-add it in the sort portion
                // sorting child's children
                SortElements(child);
            }
            // re-add children after sorting

            // sort by name portion, which is either the full name, 
            // or name that proceeds period that has a numeric value after the period.
            IOrderedEnumerable<XElement> childrenSortedByName = children
                    .OrderBy(child =>
                        {
                            string name;
                            int value;
                            Sortable(child, out name, out value);
                            return name;
                        });
            XElement[] sortedChildren;
            // if needed to sort numerically
            if (sortWithNumeric)
            {
                sortedChildren = childrenSortedByName
                    .ThenBy(child =>
                        {
                            string name;
                            int value;
                            Sortable(child, out name, out value);
                            return value;
                        })
                        .ToArray();
            }
            else
                sortedChildren = childrenSortedByName.ToArray();

            // re-add the sorted children
            foreach (XElement child in sortedChildren)
                root.Add(child);
        }

        public bool Sortable(XElement node, out string name, out int value)
        {
            var dot = new char[] { '.' };
            name = node.Name.ToString();
            if (name.Contains("."))
            {
                string[] parts = name.Split(dot);
                if (Int32.TryParse(parts[1], out value))
                {
                    name = parts[0];
                    return true;
                }
            }
            value = -1;
            return false;
        }
    }
}

Someone may be able to write this cleaner and meaner, but this should get you going.

Was interested in your question so here is my two cents.

I've implemented IComparer<T> to handle the element comparisons and two methods which handle recursion. The code could be cleaned up a bit but I've pasted in the console application code I created to show you my solution which I think worked out well.

Edit: To make this easier to read I've broken this down into the core parts though I've left the functional console app

IComparer<T> Implementation:

public class SplitComparer : IComparer<string>
{
    public int Compare(string x, string y)
    {
        var partsOfX = x.Split('.');

        int firstNumber;
        if (partsOfX.Length > 1 && int.TryParse(partsOfX[1], out firstNumber))
        {
            var secondNumber = Convert.ToInt32(y.Split('.')[1]);

            return firstNumber.CompareTo(secondNumber);
        }

        return x.CompareTo(y);
    }
}

Methods for handling the recursion:

private static XElement Sort(XElement element)
{
    var xe = new XElement(element.Name, element.Elements().OrderBy(x => x.Name.ToString(), new SplitComparer()).Select(x => Sort(x)));

    if (!xe.HasElements)
    {
        xe.Value = element.Value;
    }

    return xe;
}

private static XDocument Sort(XDocument file)
{
    return new XDocument(Sort(file.Root));
}

Functional Console Application:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Xml.Linq;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            var xml = @"<ORM_O01>
                          <ORM_O01.PATIENT>
                           <PID>      
                             <PID.18>
                               <CX.1>SecondTestFin</CX.1>
                             </PID.18>
                             <PID.3>
                                <CX.1>108</CX.1>
                             </PID.3>
                           </PID>
                          </ORM_O01.PATIENT>
                          <MSH>
                            <MSH.9>
                              <MSG.2>O01</MSG.2>
                            </MSH.9>
                            <MSH.6>
                              <HD.1>13702</HD.1>
                            </MSH.6>
                          </MSH>
                        </ORM_O01>";

            var xDoc = XDocument.Parse(xml);

            var result = Sort(xDoc);

            Console.WriteLine(result.ToString());

            Console.Read();
        }

        private static XElement Sort(XElement element)
        {
            var xe = new XElement(element.Name, element.Elements().OrderBy(x => x.Name.ToString(), new SplitComparer()).Select(x => Sort(x)));

            if (!xe.HasElements)
            {
                xe.Value = element.Value;
            }

            return xe;
        }

        private static XDocument Sort(XDocument file)
        {
            return new XDocument(Sort(file.Root));
        }
    }

    public class SplitComparer : IComparer<string>
    {
        public int Compare(string x, string y)
        {
            var partsOfX = x.Split('.');

            int firstNumber;
            if (partsOfX.Length > 1 && int.TryParse(partsOfX[1], out firstNumber))
            {
                var secondNumber = Convert.ToInt32(y.Split('.')[1]);

                return firstNumber.CompareTo(secondNumber);
            }

            return x.CompareTo(y);
        }
    }
}

utilizing System.Xml.Linq , this code may help you.

Usage:

string xmlString=
    @"
    ....your string.....
    ";

XDocument xDoc = XDocument.Load(new StringReader(xmlString));
XDocument newXDoc = SortXml(xDoc);
Console.WriteLine(newXDoc);

SortXml function:

XDocument SortXml(XDocument xDoc)
{
    Func<XElement, string> keyBuilder = 
       s => s.Name.ToString().Split('.')
             .Aggregate("",(sum, str) => sum += str.PadLeft(32,' '));

    XElement root = new XElement(xDoc.Root.Name);
    SortXml(root, xDoc.Elements(), keyBuilder);
    return new XDocument(root);
}

void SortXml(XElement newXDoc, IEnumerable<XElement> elems, Func<XElement, string> keyBuilder)
{
    foreach (var newElem in elems.OrderBy(e => keyBuilder(e)))
    {
        XElement t = new XElement(newElem);
        t.RemoveNodes();
        newXDoc.Add(t);
        SortXml(t, newElem.Elements(), keyBuilder);
    }
}

Yet another attempt, using a modified Dotnet.Commons .Xml.

Get the XmlUtils class here .

Create a custom Comparer that has all your logic (this will get you going)

public class CustomComparer : IComparer
{
    public int Compare(object x, object y)
    {
        string o1 = x as string;
        string o2 = y as string;

        string[] parts1 = o1.Split('.');
        string[] parts2 = o2.Split('.');

        // Assuming first part is alpha, last part is numeric and both of them has second part. Otherwise compare original ones.
        if (parts1.Length < 2 || parts2.Length < 2)
            return o1.CompareTo(o2);

        if (parts1[0].Equals(parts2[0]))
        {
            // Do a numeric compare
            return int.Parse(parts1[parts1.Length - 1]).CompareTo(int.Parse(parts2[parts2.Length - 1]));
        }
        else
        {
            // Just compare the first part
            return parts1[0].CompareTo(parts2[0]);
        }
    }

Then modify the XmlUtils SortElements function, add this to the top:

CustomComparer comparer = new CustomComparer();

and change the line:

if (String.Compare(node.ChildNodes[i].Name, node.ChildNodes[i-1].Name, true) < 0)

to

if (comparer.Compare(node.ChildNodes[i].Name, node.ChildNodes[i - 1].Name) < 0)

这个来自Java2S.com的解决方案会有用吗?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM