THURSDAY, JANUARY 19, 2017

Comparing the Differences Between Two Strings

In the Crossfuze Knowledge Turnkey, we’ve got a lot of great functionality. There are hundreds of updates that take the Knowledge Base to a whole new level. This includes full version tracking and the ability to revert to past versions for knowledge articles. When you’re looking at the various versions of a long article, however, it can be like trying to find a needle in a haystack to identify what’s actually changed between the versions.

After a bit of digging I was able to figure out how ServiceNow was doing those types of comparisons for update sets and versions of Business Rules, UI Pages, etc. They’ve got a number of different script includes that take the different XML updates and then compare the various parts of them. Down under the hood a little ways there is a class that they have to compare between actual text values. Finding this was the jackpot since it allowed comparing just those things that I specifically wanted to compare. Using this class ended up being a relatively straight forward process, but did require some scripting (including JellyScript) so it may not be for the faint of heart.

Here’s what it will look like in the end:

Here’s what it will look like in the end.

Here’s what it will look like in the end.

The biggest part of the challenge is handled by ServiceNow’s “Differ” script include. This class has one main function that is used to do the magic: diff.

The diff function takes in four parameters:

  • text1 – The first string that will be used in the comparison
  • text2 – The second string that will be used in the comparison
  • name – The label for what you’re comparing. Most likely going to be the field label for the field that you’re comparing. This shows up in a column on the left of the comparison
  • change – An optional parameter that indicates whether to return a result if the strings match

The end result of this is a string with part of a table that contains the strings compared line by line just like you see when comparing differences in an update set preview. It is assumed that the results of this function will be included as part of a table given a specific class so that the rows are formatted correctly. There is an XML header that needs to be removed from the result but essentially this is the code needed to use the result of the function:

<table class="diff">
    <thead>
        <tr>
            <th class="top_left_field"></th>
            <th class="texttitle" colspan="2">Left Label</th>
            <th class="texttitle" colspan="2">Right Label</th>
        </tr>
    </thead>
    diff_output
</table>

The left and right labels identify what it is that you’re comparing and diff_output is the output from the function.

There are a few different ways this functionality can be used; after looking at it I opted to use a GlideDialog window and UI Page to provide a similar experience to what is already used in other places. To do this a UI Action needs to be set up to open the dialog window and a UI Page needs to be set up to do the comparison and show the results.

Here’s what’s needed in the UI Action. It’s relatively generic other than the UI Page that is referenced. For the fields that I haven’t included you can use the default or adjust it as you see fit:

Name: Compare to Current
Table: table that has the version info you want to compare to the current record
Client: true
Form link: true
List context menu: true
Onclick: showTheComparison()
Script:

function showTheComparison(){
    //Get the values to pass into the dialog
    var sysId;
    // Check if this is called from a List, if so, rowSysId will have the Sys ID of the record
    if (typeof rowSysId == 'undefined'){
        // If it's called from a form, get the Sys ID of the record being shown
        sysId = gel('sys_uniqueValue').value;
    } else {
        sysId = rowSysId;
    }
    //Initialize and open the dialog
    //Instantiate the comparison dialog, the parameter is the name of your UI Page
    var dialog = new GlideDialogWindow("compare_kb_to_version");
    //Set the dialog title
    dialog.setTitle("Version Comparison");
    // Pass in the Sys ID of the record to compare
    dialog.setPreference("version_sys_id", sysId);
    // Set the size
    dialog.setSize('95%', '95%');
    //Open the dialog
    dialog.render();
}

 

The UI Page has a little more to it since it needs to reference the specific tables and fields where the two versions are stored.

Name: compare_kb_to_version
HTML:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<g:evaluate>
    /*
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Diff magic using the SN Differ class
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    */
    // variable to aggregate the differences for the fields into
    var diff = "";
    // get the SN object that does the comparing
    var differ = new Differ();
   
    /*
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Values needed for the diff
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    */
    var left_label = "Published Article";
    var right_label = "";
   
    // Get the version info from the dialog property
    var version_sys_id = RP.getWindowProperties().get('version_sys_id');
    var version = new GlideRecord("kb_submission");
    if (version.get(version_sys_id)) {
        // Set the label for the right side of the difference
        right_label = "Version: " + version.number;

        /*~~~~~ Short Description field ~~~~~*/
        // Pass in the strings that need comparing
        // u_kb_article is the reference field that refers to the KB article that the version is for
        var kb_short_description = differ.diff(version.u_kb_article.short_description, version.short_description, version.short_description.getLabel(), true);
        // Strip off the XML header
        var starter = kb_short_description.indexOf("?>");
        kb_short_description = kb_short_description.substring(starter + 2);
        diff += kb_short_description;
       
        /*~~~~~ Text field ~~~~~*/
        // Pass in the strings that need comparing
        var kb_text = differ.diff(version.u_kb_article.text, version.text, version.text.getLabel(), true);
        // Strip off the XML header
        var starter = kb_text.indexOf("?>");
        kb_text = kb_text.substring(starter + 2);
        diff += kb_text;
       
    }
   
</g:evaluate>
<table class="diff">
    <thead>
        <tr>
            <th class="top_left_field"></th>
            <th class="texttitle" colspan="2">${left_label}</th>
            <th class="texttitle" colspan="2">${right_label}</th>
        </tr>
    </thead>
    <g:no_escape>${diff}</g:no_escape>
</table>

</j:jelly>

The first section of code in the g:evaluate tag sets up the Differ class. Then we get the labels for each side of the comparison. Then we get the comparisons for each of the fields that we’re trying to compare. In this case we’re comparing the Short Description and the Text fields. The comparison results are cleaned up and concatenated in a string to be used inside the wrapper HTML code that sets up the header and styles.

Once you’ve got all of that in place, seeing the differences is just a click or two away.

2 Comments

Kalai 05-05-2014, 04:40

I have also tried using the same Diff script to compare the scripts in different instance much like the version comparison we have in service-now. But this is a awesome way of using the same script for a entirely different purpose 🙂 Thanks for sharing this.

Reply
MG 16-12-2014, 11:54

Have you had any success defining the widths of the $[diff] element? It seems that no matter what I try, it overrides my width definitions and ends up having the “old value” column way too small.

Reply

Leave a Reply


Latest Comments

  • David: It appears that I can hit sys_properties table with REST. This works, but I haven’t yet discovered the...
  • Mark Stanger: Hey David, It doesn’t surprise me that scoped apps have made this more difficult. I’m not...
  • David: Mark, do you have an example of how to do this in a scoped app? It seems there are many hoops to jump through...
  • Mark Stanger: The only possibility is to create a system property to override this in your application. Check out the...