views:

352

answers:

2

Hi,

I am trying to map a JavaScript sparse array to a C# representation.

What is the recommended way of doing this?

It am considering using a dictionary containing the list of oridinals that contained a value in the original array.

Any other ideas?

thanks!

+1  A: 

NOTE

I've come up with two .NET solutions to this scenario. Because both solutions require the same JavaScript code, I've only included the JavaScript code for the 2 solutions in this answer.

Now on to the answer...


EDIT Note: I apologize for not completely understanding your question at first. I never heard the term "Sparse Array" used before, so I had to look it up and found the text book definition from Wikipedia, which isn't quite what you were describing and, as far as usage I've seen elsewhere since then, doesn't seem to be what you were describing. It does make sense to use it in this way however.

Thinking about this scenario more has driven me to come up with a solution. As you mentioned, C# (and .NET in general) has no concept of undefined, other than null.

From what I understand, you're looking to be able to represent three different things in your array:

  • A valid value for the array element
  • A null value for the array element
  • The concept of an "undefined" array element

As you pointed out, C# has no concept of "undefined", other than null. To .NET, undefined would be the same as null and it probably should be kept that way. But since you obviously need a way to represent this anyway (I'm assuming for some strange business rule), I tried to come up with an implementation that will work. I'd strongly recommend not doing this though!

The JavaScript Side

The first challenge is the act of serializing the array in JavaScript. When converting an array to JSON format, undefined elements in an array are automatically converted to null. If you call JSON.stringify(...), this will happen:

var array = [73,42,undefined,null,23];
var json = JSON.stringify(array);
if(json === "[73,42,null,null,23]")
    alert('I am always true!');

The example above shows that undefined elements in the array are serialized as "null". If you really need to store undefined, you'd have to write a conversion method that will manually serialize arrays to have undefined elements. The ideal solution would result in this JSON:

"[73,42,undefined,null,23]"

This will not work however. Neither the .NET JavaScriptSerializer, or JSON.parse(...) can parse this string (although JavaScript's eval(...) method will work). What we need is a way to represent "undefined" that will serialize using standard JSON methods, and can be understood by .NET's JavaScriptSerializer. The trick to doing this is coming up with an actual object to represent undefined. To represent this, I used this JSON string:

"{undefined:null}"

An object whose only property is called "undefined", and whose value is null. This most likely will not exist in any object in JavaScript code so I'm using this as I consider it a "unique" enough flag to represent our non-existent "undefined" array element.

To serialize JSON like this, you need to provide a replacer that will do the job, so here is how you'd do that:

var arr = [73,42,undefined,null,23];
var json = JSON.stringify(arr, function(key, value) {
    jsonArray = value;
    if(value instanceof Array) { 
     var jsonArray = "[";
     for(var i = 0; i < value.length; i++) {
      var val = value[i];
      if(typeof val === "undefined") {
       jsonArray += "{undefined:null}";
      } else {
       jsonArray += JSON.stringify(value[i]);
      }
      if(i < value.length - 1) {
       jsonArray += ",";
      }
     }
     jsonArray += "]";
     if(key != null && key != "") {
      return key + ":" + jsonArray;
     } else {
      return jsonArray;
     }
    }

    if(key != null && key != "") {
     return key + ":" + JSON.stringify(jsonArray);
    } else {
     return JSON.stringify(jsonArray);
    }
});

This will get you a JSON string that looks like this:

"[73,42,{undefined:null},null,23]"

The downside to this is that once it is deserialized, the undefined array element is no longer undefined, but another object. This means that your parser will have to handle this specially. Make sure that you don't try to use this in any JSON parser that isn't aware of the completely made up "undefined" object. The next step is handle it in C#.


The C# Side

The challenge here is how to represent Undefined values. The solution below opts to treat undefined indexes as "non-existent" in an array. My other solution creates a wrapper similar to the Nullable<T> wrapper for structs.

Create a SparseArray<T> Dictionary Wrapper

I created a SparseArray<T> class that can be used just like a normal array in .NET. The difference between this class & a normal array is that when you access an array element which is undefined, it throws a custom IndexNotFoundException. You can avoid throwing this exception by first checking if the element is defined by calling SparseArray<T>.ContainsIndex(index) to check if it has that index.

Internally it uses a Dictionary, as you mentioned in your question. When you initialize it, first the constructor takes the JSON string and runs it through a JavaScriptSerializer.

It then takes the deserialized array and begins to add each array element to its _Array dictionary. As it adds elemnts, it looks for the {undefined:null} object we defined in JavaScript when we "strungified" it (not sure if that's the correct past tense there...). If it sees this object, the index is skipped over. The length of the array will increment as undefined values are found, however their index is skipped over.

Since the class' code is rather long, I'll put the usage example first:

string json = "[4,5,null,62,{undefined:null},1,68,null, 3]";
SparseArray<int?> arr = new SparseArray<int?>(json);

In a for { } loop, you'll need to check if the array contains each index before accessing it. In a foreach { } loop, the enumerator holds only defined values, so you won't need to worry about the occurrence of undefined values.

Here is the code for my SparseArray<T> class:

[DebuggerDisplay("Count = {Count}")]
public class SparseArray<T>: IList<T>
{
    Dictionary<int, T> _Array = new Dictionary<int, T>();
    int _Length = 0;

    public SparseArray(string jsonArray)
    {
        var jss = new JavaScriptSerializer();
        var objs = jss.Deserialize<object[]>(jsonArray);

        for (int i = 0; i < objs.Length; i++)
        {
            if (objs[i] is Dictionary<string, object>)
            {
                // If the undefined object {undefined:null} is found, don't add the element
                var undefined = (Dictionary<string, object>)objs[i];
                if (undefined.ContainsKey("undefined") && undefined["undefined"] == null)
                {
                    _Length++;
                    continue;
                }
            }
            T val;
            // The object being must be serializable by the JavaScriptSerializer
            // Or at the very least, be convertible from one type to another
            // by implementing IConvertible.
            try
            {
                val = (T)objs[i];
            }
            catch (InvalidCastException)
            {
                val = (T)Convert.ChangeType(objs[i], typeof(T));
            }

            _Array.Add(_Length, val);
            _Length++;
        }
    }

    public SparseArray(int length)
    {
        // Initializes the array so it behaves the same way as a standard array when initialized.
        for (int i = 0; i < length; i++)
        {
            _Array.Add(i, default(T));
        }
        _Length = length;
    }

    public bool ContainsIndex(int index)
    {
        return _Array.ContainsKey(index);
    }


    #region IList<T> Members

    public int IndexOf(T item)
    {
        foreach (KeyValuePair<int, T> pair in _Array)
        {
            if (pair.Value.Equals(item))
                return pair.Key;
        }
        return -1;
    }

    public T this[int index]
    {
        get {
            if (_Array.ContainsKey(index))
                return _Array[index];
            else
                throw new IndexNotFoundException(index);
        }
        set { _Array[index] = value; }
    }              

    public void Insert(int index, T item)
    {
        throw new NotImplementedException();
    }

    public void RemoveAt(int index)
    {
        throw new NotImplementedException();
    }

    #endregion

    #region ICollection<T> Members

    public void Add(T item)
    {
        _Array.Add(_Length, item);
        _Length++;
    }

    public void Clear()
    {
        _Array.Clear();
        _Length = 0;
    }

    public bool Contains(T item)
    {
        return _Array.ContainsValue(item);
    }        

    public int Count
    {
        get { return _Length; }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public bool Remove(T item)
    {
        throw new NotImplementedException();
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        throw new NotImplementedException();
    }

    #endregion

    #region IEnumerable<T> Members

    public IEnumerator<T> GetEnumerator()
    {
        return _Array.Values.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _Array.Values.GetEnumerator();
    }

    #endregion
}

And the exception needed to use this class:

public class IndexNotFoundException:Exception
{
    public IndexNotFoundException() { }
    public IndexNotFoundException(int index) 
          : base(string.Format("Array is undefined at position [{0}]", index)) 
    { 
    }
}
Dan Herbert
Thanks for the detailed reply, but a = [3,5,null,62] and b = [3,5,,62] are entirely different things: a[2] is null while b[2] is undefined. As C# has no concept of "undefined" (that I know of), and null is meaningful in its own right, there needs to be some other way of representing the undefined value. This is where I'm looking for recommendations.[Obviously, if I've missed something... please let me know! :)]
Andrew
I've updated my answer so that in matches what you meant. I apologize for misunderstanding your question at first.
Dan Herbert
A: 

NOTE

I've come up with two .NET solutions to this scenario. I've kept them in separate answers so they could be voted on based on which solution is more favorable, and so that the author may choose the most favorable solution as the "Accepted Answer". Because both solutions require the same JavaScript code, I've only included the JavaScript code for the 2 solutions in my other answer. If this answer is marked as accepted, I'll include my JavaScript solution in this answer so that it will be included with the answer that comes logically first on the page.

Now on to the answer...


To start, I want to repeat what I mentioned in my other solution, since it is important to mention:

As you pointed out, C# has no concept of "undefined", other than null. To .NET, undefined would be the same as null and it probably should be kept that way. But since you obviously need a way to represent this anyway (I'm assuming for some strange business rule), I tried to come up with an implementation that will work. I'd strongly recommend not doing this though!

For this solution, I created a wrapper class called Undefined<T>. It works similarly to .NET's native Nullable<T>. The downside to this is that, because .NET has no concept of "undefined", it was difficult to decide how to handle accessing the value of the object. I chose to throw an exception if you try to cast an undefined object when it is undefined.

With this solution, you have an actual array where every element exists. Although every element exists, you can't actually get the value of every element. The class I created behaves like Nullable<T> in every way, except that when you try to cast an undefined object, or get its Value, this class throws an exception. You need to call IsDefined to make sure you can use the Value before you actually try to use it.

[DebuggerDisplay("IsDefined:{IsDefined}, Value:{_Value}")]
public sealed class Undefined<T>
{
    public static Undefined<T>[] DeserializeArray(string jsonArray)
    {
        var jss = new JavaScriptSerializer();
        var objs = jss.Deserialize<object[]>(jsonArray);
        var undefinedArray = new Undefined<T>[objs.Length];

        for (int i = 0; i < objs.Length; i++)
        {
            if (objs[i] is Dictionary<string, object>)
            {
                var undefined = (Dictionary<string, object>)objs[i];
                if (undefined.ContainsKey("undefined") && undefined["undefined"] == null)
                {
                    undefinedArray[i] = new Undefined<T>(default(T), false);
                    continue;
                }
            }
            T val;
            // The object being must be serializable by the JavaScriptSerializer
            // Or at the very least, be convertible from one type to another
            // by implementing IConvertible.
            try
            {
                val = (T)objs[i];
            }
            catch (InvalidCastException)
            {
                val = (T)Convert.ChangeType(objs[i], typeof(T));
            }

            undefinedArray[i] = new Undefined<T>(val, true);
        }

        return undefinedArray;

    }

    private Undefined(T value, bool isDefined)
    {
        Value = value;
        IsDefined = isDefined;
    }

    public static explicit operator T(Undefined<T> value)
    {
        if (!value.IsDefined)
            throw new InvalidCastException("Value is undefined. Unable to cast.");
        return value.Value;
    }

    public bool IsDefined { get; private set; }

    private T _Value;
    public T Value
    {
        get
        {
            if (IsDefined)
                return _Value;
            throw new Exception("Value is undefined.");
        }
        private set { _Value = value; }
    }

    public override bool Equals(object other)
    {
        Undefined<T> o = other as Undefined<T>;
        if (o == null)
            return false;
        if ((!this.IsDefined && o.IsDefined) || this.IsDefined && !o.IsDefined)
            return false;
        return this.Value.Equals(o.Value);
    }

    public override int GetHashCode()
    {
        if (IsDefined)
            return Value.GetHashCode();
        return base.GetHashCode();
    }

    public T GetValueOrDefault()
    {
        return GetValueOrDefault(default(T));
    }

    public T GetValueOrDefault(T defaultValue)
    {
        if (IsDefined)
            return Value;
        return defaultValue;
    }

    public override string ToString()
    {
        if (IsDefined)
            Value.ToString();
        return base.ToString();
    }
}
Dan Herbert