Browse Source

Fixing numbers serialization

Sebastien Ros 11 years ago
parent
commit
8e5d51745a
4 changed files with 257 additions and 121 deletions
  1. 35 0
      Jint.Tests/Runtime/EngineTests.cs
  2. 201 115
      Jint/Native/Number/NumberPrototype.cs
  3. 15 1
      Jint/Options.cs
  4. 6 5
      Jint/Runtime/TypeConverter.cs

+ 35 - 0
Jint.Tests/Runtime/EngineTests.cs

@@ -1,6 +1,7 @@
 using System;
 using System.IO;
 using System.Reflection;
+using Jint.Native.Number;
 using Jint.Runtime;
 using Xunit;
 using Xunit.Extensions;
@@ -590,5 +591,39 @@ namespace Jint.Tests.Runtime
                 () => new Engine(cfg => cfg.MaxStatements(100)).Execute("while(true);")
             );
         }
+
+        [Fact]
+        public void ShouldConvertDoubleToStringWithoutLosingPrecision()
+        {
+            RunTest(@"
+                assert(String(14.915832707045631) === '14.915832707045631');
+                assert(String(-14.915832707045631) === '-14.915832707045631');
+                assert(String(0.5) === '0.5');
+                assert(String(0.00000001) === '1e-8');
+                assert(String(-1.0) === '-1');
+            ");
+        }
+
+        [Fact]
+        public void ShouldWriteNumbersUsingBases()
+        {
+            RunTest(@"
+                assert(15.0.toString() === '15');
+                assert(15.0.toString(2) === '1111');
+                assert(15.0.toString(8) === '17');
+                assert(15.0.toString(16) === 'f');
+                assert(15.0.toString(17) === 'f');
+                assert(15.0.toString(36) === 'f');
+                assert(15.1.toString(36) === 'f.3llllllllkau6snqkpygsc3di');
+            ");
+        }
+
+        [Fact]
+        public void ShouldComputeFractionInBase()
+        {
+            Assert.Equal("011", NumberPrototype.ToFractionBase(0.375, 2));
+            Assert.Equal("14141414141414141414141414141414141414141414141414", NumberPrototype.ToFractionBase(0.375, 5));
+        }
+
     }
 }

+ 201 - 115
Jint/Native/Number/NumberPrototype.cs

@@ -1,4 +1,6 @@
 using System;
+using System.Globalization;
+using System.Text;
 using Jint.Runtime;
 using Jint.Runtime.Interop;
 
@@ -36,9 +38,41 @@ namespace Jint.Native.Number
             FastAddProperty("toPrecision", new ClrFunctionInstance(Engine, ToPrecision), true, false, true);
         }
 
-        private JsValue ToLocaleString(JsValue thisObj, JsValue[] arguments)
+        private JsValue ToLocaleString(JsValue thisObject, JsValue[] arguments)
         {
-            throw new System.NotImplementedException();
+            if (!thisObject.IsNumber() && (thisObject.TryCast<NumberInstance>() == null))
+            {
+                throw new JavaScriptException(Engine.TypeError);
+            }
+
+            var m = TypeConverter.ToNumber(thisObject);
+
+            if (double.IsNaN(m))
+            {
+                return "NaN";
+            }
+
+            if (m.Equals(0))
+            {
+                return "0";
+            }
+
+            if (m < 0)
+            {
+                return "-" + ToNumberString(-m);
+            }
+
+            if (double.IsPositiveInfinity(m) || m >= double.MaxValue)
+            {
+                return "Infinity";
+            }
+
+            if (double.IsNegativeInfinity(m) || m <= -double.MaxValue)
+            {
+                return "-Infinity";
+            }
+
+            return m.ToString("n", Engine.Options.GetCulture());
         }
 
         private JsValue ValueOf(JsValue thisObj, JsValue[] arguments)
@@ -52,6 +86,8 @@ namespace Jint.Native.Number
             return number.PrimitiveValue;
         }
 
+        private const double Ten21 = 1e21;
+
         private JsValue ToFixed(JsValue thisObj, JsValue[] arguments)
         {
             var f = (int)TypeConverter.ToInteger(arguments.At(0, 0));
@@ -67,61 +103,71 @@ namespace Jint.Native.Number
                 return "NaN";
             }
 
-            var sign = "";
-            if (x < 0)
+            if (x >= Ten21)
             {
-                sign = "-";
-                x = -x;
+                return ToNumberString(x);
             }
+    
+            var l = (long) x; // extract integer part
 
-            string m = "";
-            if (x >= System.Math.Pow(10, 21))
-            {
-                m = TypeConverter.ToString(x);
-            }
-            else
+            if (f == 0)
             {
-                var d = (Decimal)x;
-                var significants = GetSignificantDigitCount(d);
-                var s = significants.Item1;
-                var kdigits = (int)System.Math.Floor(System.Math.Log10((double)s) + 1);
-                var n = kdigits - significants.Item2;
-
-                if (n == 0)
-                {
-                    m = "0";
-                }
-                else
-                {
-                    m = (System.Math.Round(x, f) * System.Math.Pow(10,f)).ToString();
-                }
-
-                if (f != 0)
-                {
-                    var k = m.Length;
-                    if (k <= f)
-                    {
-                        var z = new System.String('0', f + 1 - k);
-                        m += z;
-                        k = f + 1;
-                    }
-                    var a = m.Substring(0, k - f);
-                    var b = m.Substring(k - f);
-                    m = a + "." + b;
-                }
+                return l.ToString(CultureInfo.InvariantCulture);
             }
 
-            return sign + m;
+            var d = x - l;
+            return l.ToString(CultureInfo.InvariantCulture) + d.ToString("." + new string('0', f), CultureInfo.InvariantCulture);
         }
 
         private JsValue ToExponential(JsValue thisObj, JsValue[] arguments)
         {
-            throw new System.NotImplementedException();
+            var f = (int)TypeConverter.ToInteger(arguments.At(0, 16));
+            if (f < 0 || f > 20)
+            {
+                throw new JavaScriptException(Engine.RangeError, "fractionDigits argument must be between 0 and 20");
+            }
+
+            var x = TypeConverter.ToNumber(thisObj);
+
+            if (double.IsNaN(x))
+            {
+                return "NaN";
+            }
+
+            string format = System.String.Concat("#.", new System.String('0', f), "e+0");
+            return x.ToString(format, CultureInfo.InvariantCulture);
         }
 
         private JsValue ToPrecision(JsValue thisObj, JsValue[] arguments)
         {
-            throw new System.NotImplementedException();
+            var x = TypeConverter.ToNumber(thisObj);
+
+            if (arguments.At(0) == Undefined.Instance)
+            {
+                return TypeConverter.ToString(x);
+            }
+
+            var p = TypeConverter.ToInteger(arguments.At(0));
+
+            if (double.IsInfinity(x) || double.IsNaN(x))
+            {
+                return TypeConverter.ToString(x);
+            }
+
+            if (p < 1 || p > 21)
+            {
+                throw new JavaScriptException(Engine.RangeError, "precision must be between 1 and 21");
+            }
+
+            // Get the number of decimals
+            string str = x.ToString("e23", CultureInfo.InvariantCulture);
+            int decimals = str.IndexOfAny(new [] { '.', 'e' });
+            decimals = decimals == -1 ? str.Length : decimals;
+
+            p -= decimals;
+            p = p < 1 ? 1 : p;
+
+            return x.ToString("f" + p, CultureInfo.InvariantCulture);
         }
 
         private JsValue ToNumberString(JsValue thisObject, JsValue[] arguments)
@@ -131,127 +177,167 @@ namespace Jint.Native.Number
                 throw new JavaScriptException(Engine.TypeError);
             }
 
-            return ToNumberString(TypeConverter.ToNumber(thisObject));
-        }
+            var radix = arguments.At(0) == JsValue.Undefined ? 10 : (int) TypeConverter.ToInteger(arguments.At(0));
 
-        public static string ToNumberString(double m) 
-        {
-            if (double.IsNaN(m))
+            if (radix < 2 || radix > 36)
             {
-                return "NaN";
+                throw new JavaScriptException(Engine.RangeError, "radix must be between 2 and 36");
             }
 
-            if (m == 0)
+            var x = TypeConverter.ToNumber(thisObject);
+
+            if (double.IsNaN(x))
             {
-                return "0";
+                return "NaN";
             }
 
-            if (m < 0)
+            if (x.Equals(0))
             {
-                return "-" + ToNumberString(-m);
+                return "0";
             }
 
-            if (double.IsPositiveInfinity(m) || m >= double.MaxValue)
+            if (double.IsPositiveInfinity(x) || x >= double.MaxValue)
             {
                 return "Infinity";
             }
 
-            if (double.IsNegativeInfinity(m) || m <= -double.MaxValue)
+            if (x < 0)
             {
-                return "-Infinity";
+                return "-" + ToNumberString(-x, arguments);
             }
 
-            // s is all digits (significand or mantissa)
-            // k number of digits of s
-            // n total of digits in fraction s*10^n-k=m
-            // 123.4 s=1234, k=4, n=3
-            // 1234000 s = 1234, k=4, n=7
-            var d = (Decimal)m;
-            var significants = GetSignificantDigitCount(d);
-            var s = significants.Item1;
-            var k = (int)System.Math.Floor(System.Math.Log10((double)s) + 1);
-            var n = k - significants.Item2;
-            if (m < 1 && m > -1)
+            if (radix == 10)
             {
-                n++;
+                return ToNumberString(x);    
             }
 
-            while (s % 10 == 0)
+            const string format = "0.00000000000000000e0";
+            var parts = x.ToString(format, CultureInfo.InvariantCulture).Split('e');
+            var s = parts[0].TrimEnd('0').Replace(".", "");
+            var n = int.Parse(parts[1]) + 1;
+
+            var integerPart = s.Substring(0, n);
+            
+            var integer = long.Parse(integerPart);
+            var fraction = x -  integer;
+
+            string result = ToBase(integer, radix);
+            if (!fraction.Equals(0))
             {
-                s = s / 10;
-                k--;
+                result += "." + ToFractionBase(fraction, radix);
             }
 
+            return result;
+        }
 
-            if (k <= n && n <= 21)
+        public static string ToBase(long n, int radix)
+        {
+            const string digits = "0123456789abcdefghijklmnopqrstuvwxyz";
+            if (n == 0)
             {
-                return s + new string('0', n - k);
+                return "0";
             }
 
-            if (0 < n && n <= 21)
+            var result = new StringBuilder();
+            while (n > 0)
             {
-                return s.ToString().Substring(0, n) + '.' + s.ToString().Substring(n);
+                var digit = (int)n % radix;
+                n = n / radix;
+                result.Insert(0, digits[digit].ToString());
             }
 
-            if (-6 < n && n <= 0)
+            return result.ToString();
+        }
+
+        public static string ToFractionBase(double n, int radix)
+        {
+            // based on the repeated multiplication method
+            // http://www.mathpath.org/concepts/Num/frac.htm
+
+            const string digits = "0123456789abcdefghijklmnopqrstuvwxyz";
+            if (n.Equals(0))
             {
-                return "0." + new string('0', -n) + s;
+                return "0";
             }
 
-            if (k == 1)
+            var result = new StringBuilder();
+            while (n > 0 && result.Length < 50) // arbitrary limit
             {
-                return s + "e" + (n - 1 < 0 ? "-" : "+") + System.Math.Abs(n - 1);
+                var c = n*radix;
+                var d = (int) c;
+                n = c - d;
+
+                result.Append(digits[d].ToString());
             }
 
-            return s.ToString().Substring(0, 1) + "." + s.ToString().Substring(1) + "e" + (n - 1 < 0 ? "-" : "+") + System.Math.Abs(n - 1);
+            return result.ToString();
         }
 
-        public static Tuple<decimal, int> GetSignificantDigitCount(decimal value)
+        public static string ToNumberString(double m) 
         {
-            /* So, the decimal type is basically represented as a fraction of two
-             * integers: a numerator that can be anything, and a denominator that is 
-             * some power of 10.
-             * 
-             * For example, the following numbers are represented by
-             * the corresponding fractions:
-             * 
-             * VALUE    NUMERATOR   DENOMINATOR
-             * 1        1           1
-             * 1.0      10          10
-             * 1.012    1012        1000
-             * 0.04     4           100
-             * 12.01    1201        100
-             * 
-             * So basically, if the magnitude is greater than or equal to one,
-             * the number of digits is the number of digits in the numerator.
-             * If it's less than one, the number of digits is the number of     digits
-             * in the denominator.
-             */
+            if (double.IsNaN(m))
+            {
+                return "NaN";
+            }
 
-            int[] bits = decimal.GetBits(value);
-            int scalePart = bits[3];
-            int highPart = bits[2];
-            int middlePart = bits[1];
-            int lowPart = bits[0];
+            if (m.Equals(0))
+            {
+                return "0";
+            }
 
-            if (value >= 1M || value <= -1M)
+            if (double.IsPositiveInfinity(m) || m >= double.MaxValue)
             {
+                return "Infinity";
+            }
 
-                var num = new decimal(lowPart, middlePart, highPart, false, 0);
+            if (m < 0)
+            {
+                return "-" + ToNumberString(-m);
+            }
+
+            // s is all digits (significand)
+            // k number of digits of s
+            // n total of digits in fraction s*10^n-k=m
+            // 123.4 s=1234, k=4, n=3
+            // 1234000 s = 1234, k=4, n=7
+            string s = null;
+            var rFormat = m.ToString("r");
+            if (rFormat.IndexOf("e", StringComparison.OrdinalIgnoreCase) == -1)
+            {
+                s = rFormat.Replace(".", "").TrimStart('0').TrimEnd('0');
+            }
+        
+            const string format = "0.00000000000000000e0";
+            var parts = m.ToString(format, CultureInfo.InvariantCulture).Split('e');
+            if (s == null)
+            {
+                s = parts[0].TrimEnd('0').Replace(".", "");
+            }
 
-                return new Tuple<decimal, int>(num, (scalePart >> 16) & 0x7fff);
+            var n = int.Parse(parts[1]) + 1;
+            var k = s.Length;
+            
+            if (k <= n && n <= 21)
+            {
+                return s + new string('0', n - k);
             }
-            else
+
+            if (0 < n && n <= 21)
             {
+                return s.Substring(0, n) + '.' + s.Substring(n);
+            }
 
-                // Accoring to MSDN, the exponent is represented by
-                // bits 16-23 (the 2nd word):
-                // http://msdn.microsoft.com/en-us/library/system.decimal.getbits.aspx
-                int exponent = (scalePart & 0x00FF0000) >> 16;
+            if (-6 < n && n <= 0)
+            {
+                return "0." + new string('0', -n) + s;
+            }
 
-                return new Tuple<decimal, int>(lowPart, exponent + 1);
+            if (k == 1)
+            {
+                return s + "e" + (n - 1 < 0 ? "-" : "+") + System.Math.Abs(n - 1);
             }
-        }
 
+            return s.Substring(0, 1) + "." + s.Substring(1) + "e" + (n - 1 < 0 ? "-" : "+") + System.Math.Abs(n - 1);
+        }
     }
 }

+ 15 - 1
Jint/Options.cs

@@ -1,4 +1,5 @@
-using Jint.Runtime.Interop;
+using System.Globalization;
+using Jint.Runtime.Interop;
 
 namespace Jint
 {
@@ -9,6 +10,7 @@ namespace Jint
         private bool _allowDebuggerStatement;
         private ITypeConverter _typeConverter = new DefaultTypeConverter();
         private int _maxStatements = 0;
+        private CultureInfo _culture = CultureInfo.CurrentCulture;
 
         /// <summary>
         /// When called, doesn't initialize the global scope.
@@ -57,6 +59,12 @@ namespace Jint
             return this;
         }
 
+        public Options Culture(CultureInfo cultureInfo)
+        {
+            _culture = cultureInfo;
+            return this;
+        }
+
         internal bool GetDiscardGlobal()
         {
             return _discardGlobal;
@@ -81,5 +89,11 @@ namespace Jint
         {
             return _maxStatements;
         }
+
+        internal CultureInfo GetCulture()
+        {
+            return _culture;
+        }
+
     }
 }

+ 6 - 5
Jint/Runtime/TypeConverter.cs

@@ -99,6 +99,12 @@ namespace Jint.Runtime
         /// <returns></returns>
         public static double ToNumber(JsValue o)
         {
+            // check number first as this is what is usually expected
+            if (o.IsNumber())
+            {
+                return o.AsNumber();
+            } 
+            
             if (o.IsObject())
             {
                 var p = o.AsObject() as IPrimitiveInstance;
@@ -108,11 +114,6 @@ namespace Jint.Runtime
                 }
             }
 
-            if (o.IsNumber())
-            {
-                return o.AsNumber();
-            }
-
             if (o == Undefined.Instance)
             {
                 return double.NaN;