Skip to content

Instantly share code, notes, and snippets.

@ritchiecarroll
Last active February 25, 2024 22:21
Show Gist options
  • Save ritchiecarroll/42909ef62e8597c58ee2301fd2a05e3c to your computer and use it in GitHub Desktop.
Save ritchiecarroll/42909ef62e8597c58ee2301fd2a05e3c to your computer and use it in GitHub Desktop.
INI to/from JSON Conversions in C#

Convert to/from INI and JSON in C#

Code:

using System;
using System.Linq;
using System.Text;
using System.Web;
using Newtonsoft.Json.Linq;

namespace Conversion
{
    /// <summary>
    /// Defines interoperability functions for INI and JSON.
    /// </summary>
    public static class IniJsonInterop
    {
        /// <summary>
        /// Performs JavaScript encoding on given string.
        /// </summary>
        /// <param name="value">The string to be encoded.</param>
        public static string JavaScriptEncode(this string text)
        {
            return HttpUtility.JavaScriptStringEncode(text ?? string.Empty);
        }
      
        /// <summary>
        /// Gets INI as JSON.
        /// </summary>
        /// <param name="value">INI source.</param>
        /// <param name="indented"><c>true</c> to indent result JSON; otherwise, <c>false</c>.</param>
        /// <param name="preserveComments"><c>true</c> to preserve comments; otherwise, <c>false</c>.</param>
        /// <returns>Converted JSON.</returns>
        public static string GetIniAsJson(string value, bool indented, bool preserveComments)
        {
            StringBuilder json = new();
            string[] lines = value.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(line => line.Trim()).ToArray();
            string section = null;
            int kvpIndex = 0, commentIndex = 0;

            void addComment(string comment, bool eol = false)
            {
                if (!preserveComments)
                    return;

                json.Append($"{(kvpIndex > 0 ? "," : "")}\"@c{++commentIndex}{(eol ? "eol" : "")}\":\"{comment.Trim().JavaScriptEncode()}\"");
                kvpIndex++;
            }

            string removeTrailingComment(string line)
            {
                // Remove any trailing comment from section line
                int index = line.IndexOf(";", StringComparison.Ordinal);

                if (index > -1)
                {
                    addComment(line.Substring(index), true);
                    line = line.Substring(0, index).Trim();
                }

                return line;
            }

            json.Append("{");

            for (int i = 0; i < lines.Length; i++)
            {
                string line = lines[i];

                if (string.IsNullOrWhiteSpace(line) || line.StartsWith(";"))
                {
                    addComment(line);
                }
                else if (line.StartsWith("["))
                {
                    line = removeTrailingComment(line);

                    if (line.EndsWith("]"))
                    {
                        if (section is not null)
                            json.Append("},");
                        else if (commentIndex > 0)
                            json.Append(",");

                        section = line.Substring(1, line.Length - 2).Trim();
                        json.Append($"\"{section.JavaScriptEncode()}\":{{");
                        kvpIndex = commentIndex = 0;
                    }
                    else
                    {
                        throw new InvalidOperationException($"INI section has an invalid format: \"{lines[i]}\"");
                    }
                }
                else
                {
                    line = removeTrailingComment(line);

                    string[] kvp = line.Split('=');

                    if (kvp.Length != 2)
                        throw new InvalidOperationException($"INI key-value entry has an invalid format: \"{lines[i]}\"");

                    json.Append($"{(kvpIndex > 0 ? "," : "")}\"{kvp[0].Trim().JavaScriptEncode()}\":\"{kvp[1].Trim().JavaScriptEncode()}\"");
                    kvpIndex++;
                }
            }

            if (section is not null)
                json.Append("}");

            json.Append("}");

            return indented ?
                JToken.Parse(json.ToString()).ToString(Newtonsoft.Json.Formatting.Indented) :
                json.ToString();
        }

        /// <summary>
        /// Gets JSON as INI.
        /// </summary>
        /// <param name="value">JSON source.</param>
        /// <param name="restoreComments"><c>true</c> to restore comments; otherwise, <c>false</c>.</param>
        /// <returns>Converted INI.</returns>
        public static string GetJsonAsIni(string value, bool restoreComments)
        {
            StringBuilder ini = new();
            JObject json = JObject.Parse(value);
            string eolComment = string.Empty;

            void writeProperty(JProperty property)
            {
                if (property is null)
                    return;

                if (property.Name.StartsWith("@c"))
                {
                    if (restoreComments)
                    {
                        if (property.Name.EndsWith("eol"))
                            eolComment = $" {property.Value}";
                        else
                            ini.AppendLine(property.Value.ToString());
                    }
                }
                else
                {
                    ini.AppendLine($"{property.Name}={property.Value}{eolComment}");
                    eolComment = string.Empty;
                }
            }

            foreach (JProperty property in json.Properties())
            {
                if (property.Value.HasValues)
                {
                    ini.AppendLine($"[{property.Name}]{eolComment}");
                    eolComment = string.Empty;

                    foreach (JToken kvp in property.Value)
                        writeProperty(kvp as JProperty);
                }
                else
                {
                    writeProperty(property);
                }
            }

            return ini.ToString();
        }
    }
}

Tests:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using static Conversion.IniJsonInterop;

namespace TestFunctions
{
    [TestClass]
    public class IniJsonTests
    {
        private static string iniInput = 
          @"; ------ PQube 3 from Powerside.
            ; ------ www.powerside.com
            ; ------ PQube 3 Version 3.8
            rootKey=R00tVal
            ;----------------------------------------------------
            [PQube_Information] ; End of section comment
            ;----------------------------------------------------
            ; ------ Assign a unique identifier for your PQube 3
            PQube_ID=""(PQube_ID not set)"" ; End of value comment

            ; ------ Describe the place where your PQube 3 is installed
            Location_Name=""(location not set)""

            ; ------ Optional additional information about your PQube 3
            Note_1=""(note not set)""
            Note_2=""(note not set)""
            [NewSection]
            ; New section
            NewKey=NewValue";

        private static string jsonInput =
          @"{
              ""@c1"": ""; ------ PQube 3 from Powerside."",
              ""@c2"": ""; ------ www.powerside.com"",
              ""@c3"": ""; ------ PQube 3 Version 3.8"",
              ""rootKey"": ""R00tVal"",
              ""@c4"": "";----------------------------------------------------"",
              ""@c5eol"": ""; End of section comment"",
              ""PQube_Information"": {
                ""@c1"": "";----------------------------------------------------"",
                ""@c2"": ""; ------ Assign a unique identifier for your PQube 3"",
                ""@c3eol"": ""; End of value comment"",
                ""PQube_ID"": ""\""(PQube_ID not set)\"""",
                ""@c4"": """",
                ""@c5"": ""; ------ Describe the place where your PQube 3 is installed"",
                ""Location_Name"": ""\""(location not set)\"""",
                ""@c6"": """",
                ""@c7"": ""; ------ Optional additional information about your PQube 3"",
                ""Note_1"": ""\""(note not set)\"""",
                ""Note_2"": ""\""(note not set)\""""
              },
              ""NewSection"": {
                ""@c1"": ""; New section"",
                ""NewKey"": ""NewValue""
              }
            }";

        [TestMethod]
        public void TestIni2Json()
        {
            string json = GetIniAsJson(iniInput, true, true);
            Console.WriteLine(json);
            Assert.IsTrue(JToken.DeepEquals(JToken.Parse(json), JToken.Parse(jsonInput)));
        }

        [TestMethod]
        public void TestJson2Ini()
        {
            string ini = GetJsonAsIni(jsonInput,true);
            Console.WriteLine(ini);

            string[] getLines(string value) {
                return value.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(line => line.Trim()).ToArray();
            }

            string[] sourceLines = getLines(ini);
            string[] targetLines = getLines(iniInput + "\r\n");

            Assert.IsTrue(sourceLines.CompareTo(targetLines) == 0);
        }
    }

    internal static class CompareExtensions
    {
        public static int CompareTo<TSource>(this TSource[] array1, TSource[] array2, bool orderIsImportant = true) =>
            array1.CompareTo(array2, Comparer<TSource>.Default, orderIsImportant);

        private static int CompareTo(this Array array1, Array array2, IComparer comparer, bool orderIsImportant = true)
        {
            if (comparer is null)
                throw new ArgumentNullException("comparer");

            if (array1 is null && array2 is null)
                return 0;

            if (array1 is null)
                return -1;

            if (array2 is null)
                return 1;

            if (array1.Rank != 1 || array2.Rank != 1)
                throw new ArgumentException("Cannot compare multidimensional arrays");

            if (array1.Length != array2.Length)
                return array1.Length.CompareTo(array2.Length);

            if (!orderIsImportant)
            {
                array1 = array1.Cast<object>().ToArray();
                array2 = array2.Cast<object>().ToArray();
                Array.Sort(array1, comparer);
                Array.Sort(array2, comparer);
            }

            int result = 0;

            for (int i = 0; i < array1.Length; i++)
            {
                result = comparer.Compare(array1.GetValue(i), array2.GetValue(i));
                
                if (result != 0)
                    break;
            }

            return result;
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment