lundi 14 septembre 2015

Nearest neighborS search by Binary Partition Tree

There are a lot of excellent references on Internet, one of them is:

The missing part: some code to play around. Let's try to make something generic. What to we need to make a class partitionable ?

    public interface IIsPartionableByBT {
        Int32 Id { get; }

        float BTX { get; }
        float BTY { get; }
        float BTZ { get; }

        float DistTo(IIsPartionableByBT c);
    }

    public delegate float BTCooProjector(IIsPartionableByBT coo);

We also need Cells/Nodes for the tree:

    public class MbtBSPNode<T> where T :  IIsPartionableByBT {
        public MbtBSPNode<T> Parent { get; set; }
        public T Cell { get; set; }
        public MbtBSPNode<T> Left { get; set; }
        public MbtBSPNode<T> Right { get; set; }

        public ICollection<MbtBSPNode<T>> Cells { get; set; }

        public UInt32 Depth { get { if (Parent == null) return 0; return Parent.Depth + 1; } }
    }

And the tree himself:

    public class MbtBSPTree<T> where T : class, IIsPartionableByBT {
        public MbtBSPNode<T> Root { get; private set; }

        public UInt16 SplitDepthBy { get; private set; }

        public UInt16 LimitDepthTo { get; private set; }

        public MbtBSPTree(IEnumerable<T> l, UInt16 splitDepthBy = 0, UInt16 limitDepthTo = 0) {
            if (SplitDepthBy == 0) {
                if (l.Min(x => x.BTZ) == l.Max(x => x.BTZ)) {
                    SplitDepthBy = 2;
                } else {
                    SplitDepthBy = 3;
                }
            } else {
                SplitDepthBy = splitDepthBy;
            }
            LimitDepthTo = limitDepthTo;

            Root = BuildBSPTree(l, 0);
        }

        public BTCooProjector DefProjector(UInt16 axis) {
            switch (axis) {
                case 0:
                    return (n) => n.BTX;
                case 1:
                    return (n) => n.BTY;
                case 2:
                    return (n) => n.BTZ;
                default:
                    throw new Exception("valeur de split de profondeur non gérée");
            }
        }

        public MbtBSPNode<T> BuildBSPTree(IEnumerable<T> l, UInt16 depth, MbtBSPNode<T> p = null) {
            if (l.Count() == 0)
                return null;
            UInt16 axis = (UInt16)(depth % SplitDepthBy);

            MbtBSPNode<T> n = new MbtBSPNode<T>();
            n.Parent = p;

            if (LimitDepthTo > 0 && depth == LimitDepthTo) {
                n.Cells = new List<MbtBSPNode<T>>();
                foreach (T cc in l) {
                    n.Cells.Add(new MbtBSPNode<T> { Cell = cc });
                }
            } else {
                Double med = Double.MinValue;
                List<T> ll = new List<T>();
                List<T> lr = new List<T>();

                BTCooProjector proj = DefProjector((UInt16)(depth % SplitDepthBy));
                med = l.Median(x => proj(x));
                foreach (T c in l) {
                    if (proj(c) < med) {
                        ll.Add(c);
                    } else {
                        if (proj(c) == med) {
                            if (n.Cell == null)
                                n.Cell = c;
                            else
                                ll.Add(c);
                        } else {
                            lr.Add(c);
                        }
                    }
                }

                if (depth <= 2) {
                    //this will lead to 8 threads
                    Task<MbtBSPNode<T>> tl = Task<MbtBSPNode<T>>.Factory.StartNew(() =>
                        BuildBSPTree(ll, (UInt16)(depth + 1), n));
                    n.Right = BuildBSPTree(lr, (UInt16)(depth + 1), n);
                    //synchro thread
                    n.Left = tl.Result;
                } else {
                    n.Left = BuildBSPTree(ll, (UInt16)(depth + 1), n);
                    n.Right = BuildBSPTree(lr, (UInt16)(depth + 1), n);
                }
            }

            return n;
        }

#if DEBUG
        public Int32 ScannedNodeNumber { get; set; }
#endif

        public IEnumerable<MbtBSPNode<T>> GetNearestNodes(T cel, float radius) {
#if DEBUG
            ScannedNodeNumber = 0;
#endif
            MbtBSPNode<T> current;
            Queue<MbtBSPNode<T>> q = new Queue<MbtBSPNode<T>>();
            q.Enqueue(Root);

            while (q.Count > 0) {
                current = q.Dequeue();
                BTCooProjector proj;

                if (current.Cell != null) {
#if DEBUG
                    ScannedNodeNumber++;
#endif
                    if (cel.DistTo(current.Cell) <= radius && cel.Id != current.Cell.Id) {
                        yield return current;
                    }

                    proj = DefProjector((UInt16)(current.Depth % SplitDepthBy));

                    if (proj(cel) - radius <= proj(current.Cell)) {
                        if (current.Left != null)
                            q.Enqueue(current.Left);
                    }
                    if (proj(cel) + radius > proj(current.Cell)) {
                        if (current.Right != null)
                            q.Enqueue(current.Right);
                    }
                }

                if (current.Cells != null) {
                    foreach (MbtBSPNode<T> cc in current.Cells) {
#if DEBUG
                        ScannedNodeNumber++;
#endif
                        if (cel.Id != cc.Cell.Id && cel.DistTo(cc.Cell) <= radius)
                            yield return cc;
                    }
                }

            }
        }

One can find a median algorithm in here:

jeudi 14 mai 2015

Coloring cells in an OpenXml SpreadsheetDocument

Tow days, and many readings. That's the time I needed to figure out how to set a cell color in an openXml spreadsheet.

It finally ends by the use of the OpenXml SDK Productivity Tool.

The main point seems to be that there must be a minimal stylesheet in the spreadsheet. Among other, this minimal stylesheet must comprise s two Fills. This styleshett may be generated by the following code:

private void GenerateWorkbookStylesPartContent(WorkbookPart workbookPart, String partId) {
    WorkbookStylesPart wsp = workbookPart.AddNewPart(partId);

    Stylesheet stylesheet = new Stylesheet() { 
        MCAttributes = new MarkupCompatibilityAttributes() { Ignorable = "x14ac" } };
    stylesheet.AddNamespaceDeclaration("mc", 
        "http://schemas.openxmlformats.org/markup-compatibility/2006");
    stylesheet.AddNamespaceDeclaration("x14ac", 
        "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac");

    Fonts fonts = new Fonts() { Count = (UInt32Value)1U, KnownFonts = true };
    Font font = new Font();
    FontSize fontSize = new FontSize() { Val = 11D };
    Color color = new Color() { Theme = (UInt32Value)1U };
    FontName fontName = new FontName() { Val = "Calibri" };
    FontFamilyNumbering fontFamilyNumbering = new FontFamilyNumbering() { Val = 2 };
    FontScheme fontScheme = new FontScheme() { Val = FontSchemeValues.Minor };
    font.Append(fontSize);
    font.Append(color);
    font.Append(fontName);
    font.Append(fontFamilyNumbering);
    font.Append(fontScheme);
    fonts.Append(font);
    stylesheet.Fonts = fonts;

    Borders borders = new Borders() { Count = (UInt32Value)1U };
    Border border = new Border();
    LeftBorder leftBorder = new LeftBorder();
    RightBorder rightBorder = new RightBorder();
    TopBorder topBorder = new TopBorder();
    BottomBorder bottomBorder = new BottomBorder();
    DiagonalBorder diagonalBorder = new DiagonalBorder();
    border.Append(leftBorder);
    border.Append(rightBorder);
    border.Append(topBorder);
    border.Append(bottomBorder);
    border.Append(diagonalBorder);
    borders.Append(border);
    stylesheet.Borders = borders;

    stylesheet.Fills = new Fills();
    Fill f = new Fill { PatternFill = 
        new PatternFill { PatternType = PatternValues.None}};
    stylesheet.Fills.Append(f);
    stylesheet.Fills.Append(new Fill { PatternFill = 
        new PatternFill { PatternType = PatternValues.Gray125 } });

    CellFormats cellFormats = new CellFormats() { Count = (UInt32Value)1U };
    CellFormat cellFormat = new CellFormat() { 
        NumberFormatId = (UInt32Value)0U, 
        FontId = (UInt32Value)0U, 
        FillId = (UInt32Value)0U, 
        BorderId = (UInt32Value)0U, 
        FormatId = (UInt32Value)0U };
    cellFormats.Append(cellFormat);
    stylesheet.CellFormats = cellFormats;

    CellStyles cellStyles = new CellStyles() { Count = (UInt32Value)1U };
    CellStyle cellStyle = new CellStyle() { 
        Name = "Normal", FormatId = (UInt32Value)0U, BuiltinId = (UInt32Value)0U };
    stylesheet.CellStyles = cellStyles;

    CellStyleFormats cellStyleFormats = new CellStyleFormats() { Count = (UInt32Value)1U };
    CellFormat cellFormat2 = new CellFormat() { 
        NumberFormatId = (UInt32Value)0U, 
        FontId = (UInt32Value)0U, 
        FillId = (UInt32Value)0U, 
        BorderId = (UInt32Value)0U };
    cellStyleFormats.Append(cellFormat2);
    stylesheet.CellStyleFormats = cellStyleFormats;

    cellStyles.Append(cellStyle);

    wsp.Stylesheet = stylesheet;
}

From here, all what remain to do is handle the color in the cells, during the process I use a dictionary to avoid querying the stylesheet. In my case the key type is System.Drawing.Color because I'm exporting a DataGridView but, of course this type can be of any type you need.

    private Dictionary _colors = 
        new Dictionary();

Then, somewhere in the code:

UInt32 cellStyleUid = 0;
if ( col != System.Drawing.Color.Transparent) {
    if (!_colors.ContainsKey(col)) {
        //that is the style does not exists for this color
        if (_ssDoc.WorkbookPart.WorkbookStylesPart == null) {
            GenerateWorkbookStylesPartContent(_ssDoc.WorkbookPart, "rId5");
        }

        //Create the Fill
        Fill fill = new Fill();
        PatternFill pf = new PatternFill { PatternType = PatternValues.Solid };
        ForegroundColor fgc = new ForegroundColor { 
             Rgb = HexBinaryValue.FromString(Convert.ToString(col.ToArgb(), 16)) };
        BackgroundColor bgc = new BackgroundColor() { Indexed = (UInt32Value)64U };
        pf.Append(fgc);
        pf.Append(bgc);
        fill.Append(pf);
        //update the stylesheet
        _ssDoc.WorkbookPart.WorkbookStylesPart.Stylesheet.Fills.Append(fill);
        Int32 iFill = _ssDoc.WorkbookPart.WorkbookStylesPart.Stylesheet.Fills.Count() - 1;
        _ssDoc.WorkbookPart.WorkbookStylesPart.Stylesheet.Fills.Count = (UInt32)(iFill + 1);

        //Create the CellFormat to use the created Fill
        CellFormat lcf = 
            (CellFormat)_ssDoc.WorkbookPart.WorkbookStylesPart.Stylesheet.CellFormats.LastChild;
        CellFormat cf = new CellFormat { 
            NumberFormatId = lcf.NumberFormatId,
            FontId = lcf.FontId,
            FillId = (UInt32Value)(UInt32)iFill,
            BorderId = lcf.BorderId,
            FormatId = lcf.FormatId,
            ApplyFill = true,
            ApplyFont = lcf.ApplyFont
        };                   

        //update the stylesheet 
        _ssDoc.WorkbookPart.WorkbookStylesPart.Stylesheet.CellFormats.Append(cf);
        Int32 iCellFormat = 
            _ssDoc.WorkbookPart.WorkbookStylesPart.Stylesheet.CellFormats.Count() - 1;
        _ssDoc.WorkbookPart.WorkbookStylesPart.Stylesheet.CellFormats.Count = 
            (UInt32)(iCellFormat + 1);

        //put the index of the new CellFormat in the buffer
        _colors.Add(col, (UInt32)iCellFormat);

    }
    //retrieve the index of the cell format for the color
    cellStyleUid = _colors[col];
}

It remains to use the updated stylesheet at the cell level. With cell being of type DocumentFormat.OpenXml.Spreadsheet.Cell

if (cellStyleUid != 0)
    cell.StyleIndex = cellStyleUid;

Et voilà !

lundi 27 avril 2015

Secure connection strings in app.config for a desktop application

The goal is to get away as little as possible of the standard.

In this case the standard is : Encrypting Configuration Information Using Protected Configuration

In short: use a ProtectedConfigurationProvider. The .Net Framework provides two of them:

The main inconvenience of those providers comes from the fact that they have been designed for web applications: that are applications in which the securisable is the application server. That is one web.config in one machine

With a desktop application you have a plurality of app.config

Said providers are machine based, i.e. there is one configuration file per machine. In other words, the configuration file of one machine can't be used by another because of different encryption keys.

Fortunately the .Net Framework provides an abstract class that can be overridden to built a custom provider.

Here is a sample

The DLL:

using System;
using System.Configuration;
using System.Xml;

namespace MBT.ProtectedConfigurationProviders {

    public class WeakProtectedConfigurationProvider : ProtectedConfigurationProvider {        
        public override void Initialize(string name, 
            System.Collections.Specialized.NameValueCollection config) {
            
            base.Initialize(name, config);
        }

        public override System.Xml.XmlNode Decrypt(System.Xml.XmlNode encryptedNode) {
            string decryptedData =
                DecryptString(encryptedNode.InnerText);

            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.PreserveWhitespace = true;
            xmlDoc.LoadXml(decryptedData);

            return xmlDoc.DocumentElement;
        }

        public override System.Xml.XmlNode Encrypt(System.Xml.XmlNode node) {
            string encryptedData = EncryptString(node.OuterXml);

            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.PreserveWhitespace = true;
            xmlDoc.LoadXml("" +
                encryptedData + "");

            return xmlDoc.DocumentElement;
        }

        public static String EncryptString(String st) {
            return Convert.ToBase64String(WeakProtectedConfigurationProvider.GetBytes(st));
        }

        public static String DecryptString(String st) {
            return WeakProtectedConfigurationProvider.GetString(Convert.FromBase64String(st));
        }

        public static byte[] GetBytes(string str) {
            byte[] bytes = new byte[str.Length * sizeof(char)];
            System.Buffer.BlockCopy(str.ToCharArray(), 0, bytes, 0, bytes.Length);
            return bytes;
        }

        public static string GetString(byte[] bytes) {
            char[] chars = new char[bytes.Length / sizeof(char)];
            System.Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length);
            return new string(chars);
        }
    }

}

Here the WEAK word is deliberately chosen. Base 64 encoding is not at all a cypher algorithm.

Then the Program:

using System;
using System.Configuration;

namespace bas {
    class Program {
        static void Main(string[] args) {
            //String cs = @"";
            //Console.WriteLine(Convert.ToBase64String(WeakProtectedConfigurationProvider.GetBytes(cs)));
            Console.WriteLine(ConfigurationManager.ConnectionStrings["SomeName"].ConnectionString);
        }
    }
}

And finally the app.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration >
  <!-- xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"-->
   
  <configProtectedData defaultProvider="WeakProtectedConfigurationProvider">
    <providers>
      <clear />
      <add name ="WeakProtectedConfigurationProvider" type="MBT.ProtectedConfigurationProviders.WeakProtectedConfigurationProvider, WeakProtectedConfigurationProvider"/>
    </providers>
  </configProtectedData>

  <connectionStrings configProtectionProvider="WeakProtectedConfigurationProvider">
    <EncryptedData>
      <CipherData>
        <CipherValue>PABjAG8AbgBuAGUAYwB0AGkAbwBuAFMAdAByAGkAbgBnAHMAPgA8AGEAZABkACAAbgBhAG0AZQA9ACIAUwBvAG0AZQBOAGEAbQBlACIAIABjAG8AbgBuAGUAYwB0AGkAbwBuAFMAdAByAGkAbgBnAD0AIgBEAGEAdABhACAAUwBvAHUAcgBjAGUAPQBTAG8AbQBlAFMAZQByAHYAZQByADsASQBuAGkAdABpAGEAbAAgAEMAYQB0AGEAbABvAGcAPQBTAG8AbQBlAEQAYQB0AGEAQgBhAHMAZQA7AEkAbgB0AGUAZwByAGEAdABlAGQAIABTAGUAYwB1AHIAaQB0AHkAPQBUAHIAdQBlADsAQwBvAG4AbgBlAGMAdAAgAFQAaQBtAGUAbwB1AHQAPQAxADgAMAA7AE0AdQBsAHQAaQBwAGwAZQBBAGMAdABpAHYAZQBSAGUAcwB1AGwAdABTAGUAdABzAD0AVAByAHUAZQAiACAAcAByAG8AdgBpAGQAZQByAE4AYQBtAGUAPQAiAFMAeQBzAHQAZQBtAC4ARABhAHQAYQAuAFMAcQBsAEMAbABpAGUAbgB0ACIAIAAvAD4APAAvAGMAbwBuAG4AZQBjAHQAaQBvAG4AUwB0AHIAaQBuAGcAcwA+AA==</CipherValue>
      </CipherData>
    </EncryptedData>
  </connectionStrings>
  
</configuration>

At least two points are significant here. First:

<add name ="WeakProtectedConfigurationProvider"
    type="MBT.ProtectedConfigurationProviders.WeakProtectedConfigurationProvider,  
          WeakProtectedConfigurationProvider"/>

Here to avoid a GAC registration, on use the syntax "type, assembly"

Second:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

One may want to use a namespace in configuration tag of app.config to avoid Visual Studio complaint. But, in my case this also leads to side effect: "side by side configuration error" which prevents application from starting. So, for now, I do without, and it runs well.

Thank you to StackOverflow for the StringToBytesToString.

samedi 25 avril 2015

Reset visual studio to have it to forget you !

From time to time you may want to connect as another one to visual studio. That is not so easy as it seems. Even the official documentation is not of great help.

Said documentation points out a ResetSettings flag, but it does not do the trick.

Hopefully, and as always, StackOverflow is helpfull, and the solution is:

devenv /resetuserdata

mardi 21 avril 2015

SSRS: pass mutivalued parameter by http

In an SSRS report you may have multivaluable parameters :


In this case the question is: how to pass such a paramter by http ?
The answer is quite simple. In your query you must have a sequence like:
&CRITS=CRIT1&CRITS=CRIT2

The number of &CRITS= depends on the number of values.

lundi 19 janvier 2015

SSRS: All report in one page in the browser

In Sql Server Reporting Service, if you want to get your report without pagination you have to disable soft page break.
This can be done by setting InteractiveHeight to 0.
  1. Select the report property by clicking anywhere outside the report.
  2. Set the InteractiveSize - Height to 0

lundi 12 janvier 2015

Je suis Charlie

while ( true ) {
    Console.WriteLine("Je suis Charlie");
}

jeudi 1 janvier 2015

Merging runs in an openxml file/document

The goal is to reduce the number of runs in the paragraphs of a document. I made the choice not to use the openxml SDK. Indeed my goal is a templating engine. In my process the runs may contain xsl, that is xml. So I made the choice to stay on raw XML tools.

The operational (without the usings, except those revealing a dependency) code looks like:

namespace SandBox {
    class Program {
        static void Main(string[] args) {
            try {

                String fileName = @"somepath\somefile.docx";
                String destFile = "res.docx";
                
                Tuple<XPathNavigator, XmlNamespaceManager> xp = 
                    ZDocx.GetNavigatorAndManagerFromString(
                        ZDocx.GetDocxDocumentStringFromDocxFile(fileName));
                XPathNavigator xpn = xp.Item1;
                XmlNamespaceManager xnm = xp.Item2;

                XPathNodeIterator xni = xpn.Select("//w:p", xnm);
                while (xni.MoveNext()) {
                    //Merge all runs ignoring styles
                    //ZDocx.MergeRuns(xni.Current);
                    //Merge considering only Bold as a grouping condition
                    ZDocx.MergeRuns(xni.Current, new ByStylesNodesComparator  {
                        Settings = new ByStylesNodesComparatorSettings {
                            CheckBold = true
                    }});                    
                }

                System.IO.File.Copy(fileName, destFile, true);
                ZDocx.SetDocxDocumentStringToDocxFile(xpn, destFile);

            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
            }
        }
    }
}

There are 3 main steps:

  • I fristly load the document.xml part the openxml file in an XPathNavigator
  • Then I process each paragraph.
  • Finally I inject the modified document.xml in a new openxml file.

First the cooking code:

using Ionic.Zip;

namespace SandBox {
    public class ZDocx {
        public static String nsW = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";

        public static String GetDocxDocumentStringFromDocxFile(String fileName) {
            String tf = System.IO.Path.GetTempFileName();
            StreamWriter sw = new StreamWriter(tf);
            using (ZipFile zip = ZipFile.Read(fileName)) {
                ZipEntry e = zip["word\\document.xml"];
                e.Extract(sw.BaseStream);                
            }
            try {
                sw.Close();
            } catch { }
            
            StreamReader sr = new StreamReader(tf);
            String st = sr.ReadToEnd();
            sr.Close();
            System.IO.File.Delete(tf);
            
            return st;
        }

        public static void SetDocxDocumentStringToDocxFile(XPathNavigator xdoc, String fileName) {            
            using (ZipFile zip = ZipFile.Read(fileName)) {
                using (MemoryStream ms = new MemoryStream()) {
                    XmlWriterSettings xws = new XmlWriterSettings {
                        Encoding = Encoding.UTF8
                    };
                    using (XmlWriter xw = XmlWriter.Create(ms, xws)) {
                        xw.WriteNode(xdoc, false);
                        xw.Flush();
                        ms.Position = 0;
                        zip.UpdateEntry("word\\document.xml", ms);
                        zip.Save();
                    }
                }
            }            
        }

        public static Tuple<XPathNavigator, XmlNamespaceManager> GetNavigatorAndManagerFromString(String st) {
            XmlDocument xml = new XmlDocument();
            StringReader stReader = new StringReader(st);
            XmlReader xReader = XmlReader.Create(stReader, new XmlReaderSettings() { 
                IgnoreWhitespace = false, 
                CloseInput = true });
            xml.Load(xReader);
            xReader.Close();
            XPathNavigator navXml = xml.CreateNavigator();
            XmlNamespaceManager manager = new XmlNamespaceManager(navXml.NameTable);
            manager.AddNamespace("w", nsW);
            manager.AddNamespace("xsl", "http://www.w3.org/1999/XSL/Transform");

            return new Tuple<XPathNavigator, XmlNamespaceManager>(navXml, manager);
        }

    }
}

Then the merging code:

namespace SandBox {
    public class ZDocx {
        public static void MergeRuns(XPathNavigator paragraph, INodeComparator areMergeable = null) {
            if (paragraph.LocalName != "p")
                throw new Exception("MergeRuns: paragraph is not a 'w:p'.");
            if (areMergeable == null)
                areMergeable = AlwaysTrueNodesComparator.GetInstance();

            XPathNavigator destRun = null;
            XPathNavigator lPara = paragraph.Clone();
            lPara.MoveToFirstChild();
            do {
                if (lPara.LocalName != "r")
                    continue;

                if (destRun == null) {
                    destRun = lPara.Clone();
                    continue;
                }

                if ( areMergeable.AreMergeable(destRun, lPara) ) {
                    XPathNavigator nL = lPara.Clone();
                    XPathNavigator nK = destRun.Clone();
                    nL.MoveToChild("t", nsW);
                    nK.MoveToChild("t", nsW);
                    nK.InnerXml += nL.InnerXml;                    
                    nL.MoveToParent();
                    nL.MoveToPrevious();
                    lPara.DeleteSelf();
                    lPara = nL;
                } else {
                    destRun = lPara.Clone();
                }

            } while (lPara.MoveToNext());
        }
    }
}
In the previous code, one key is the areMergeable parameter: this parameter allows to decide how the merging occurs. This parameter implements the following interface.
    public interface INodeComparator {
        Boolean AreMergeable(XPathNavigator xpn1, XPathNavigator xpn2);
    }
This interface may be implemented as in the following samples provided as an inspiration root:
namespace SandBox {
    public class AlwaysTrueNodesComparator : INodeComparator {
        private AlwaysTrueNodesComparator() {}

        private static AlwaysTrueNodesComparator _inst = new AlwaysTrueNodesComparator();
        public static AlwaysTrueNodesComparator GetInstance() {
            return _inst;
        }

        public Boolean AreMergeable(XPathNavigator xpn1, XPathNavigator xpn2) {
            return true;
        }
    }

    public class ByStylesNodesComparatorSettings {
        public Boolean CheckBold { get; set; }
        public Boolean CheckUnderlined { get; set; }
        public Boolean CheckItalic { get; set; }
        public Boolean CheckStriked { get; set; }
    }

    public class ByStylesNodesComparator : INodeComparator {
        public ByStylesNodesComparatorSettings Settings { get; set; }

        public Boolean AreMergeable(XPathNavigator xpn1, XPathNavigator xpn2) {
            XPathNavigator nav1 = xpn1.Clone();
            XPathNavigator nav2 = xpn2.Clone();

            Boolean nav1HasRPr = nav1.MoveToChild("rPr", ZDocx.nsW);
            Boolean nav2HasRPr = nav2.MoveToChild("rPr", ZDocx.nsW);

            Boolean b1, b2;

            if (Settings == null || Settings.CheckBold) {
                b1 = nav1HasRPr && nav1.SelectChildren("b", ZDocx.nsW).Count == 1;
                b2 = nav2HasRPr && nav2.SelectChildren("b", ZDocx.nsW).Count == 1;
                if (b1 != b2)
                    return false;
            }

            if (Settings == null || Settings.CheckUnderlined) {
                b1 = nav1HasRPr && nav1.SelectChildren("u", ZDocx.nsW).Count == 1;
                b2 = nav2HasRPr && nav2.SelectChildren("u", ZDocx.nsW).Count == 1;
                if (b1 != b2)
                    return false;
            }

            if (Settings == null || Settings.CheckItalic) {
                b1 = nav1HasRPr && nav1.SelectChildren("i", ZDocx.nsW).Count == 1;
                b2 = nav2HasRPr && nav2.SelectChildren("i", ZDocx.nsW).Count == 1;
                if (b1 != b2)
                    return false;
            }

            if (Settings == null || Settings.CheckStriked) {
                b1 = nav1HasRPr && nav1.SelectChildren("strike", ZDocx.nsW).Count == 1;
                b2 = nav2HasRPr && nav2.SelectChildren("strike", ZDocx.nsW).Count == 1;
                if (b1 != b2)
                    return false;
            }

            return true;
        }
    }
}

Take care to clone the navigators in the AreMergeable method to not surprise the caller.

Using AlwaysTrueNodesComparator reduces all paragraphs to one single run with the styles (or not) of the first run of the reduced paragraph.

Using ByStylesNodesComparator allows to merge runs according to some part of their styles. In the implemented class the handled style are Bold, Underline, Strike and Italic. Be careful that only basis underlining is handled. The underline style is not handled.