Implement VBMath module (dotnet/corefx#31897)
authorIain Nicol <git@iainnicol.com>
Thu, 23 Aug 2018 16:34:48 +0000 (17:34 +0100)
committerDan Moseley <danmose@microsoft.com>
Thu, 23 Aug 2018 16:34:48 +0000 (09:34 -0700)
This module implements a VB6 compatible random number generator.

This code is a verbatim port of the Reference Source, with a couple
comments fixed.  The tests are new.

Relates to issue dotnet/corefx#31181.

Commit migrated from https://github.com/dotnet/corefx/commit/ecd0521478ee11db675acc2013124f2960244d72

src/libraries/Microsoft.VisualBasic/ref/Microsoft.VisualBasic.cs
src/libraries/Microsoft.VisualBasic/src/Microsoft.VisualBasic.vbproj
src/libraries/Microsoft.VisualBasic/src/Microsoft/VisualBasic/CompilerServices/ProjectData.vb
src/libraries/Microsoft.VisualBasic/src/Microsoft/VisualBasic/VBMath.vb [new file with mode: 0644]
src/libraries/Microsoft.VisualBasic/tests/Microsoft.VisualBasic.Tests.csproj
src/libraries/Microsoft.VisualBasic/tests/VBMathTests.cs [new file with mode: 0644]

index baa0086..bae9396 100644 (file)
@@ -164,6 +164,15 @@ namespace Microsoft.VisualBasic
         public VBFixedStringAttribute(int Length) { }
         public int Length { get { throw null; } }
     }
+    [Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute]
+    public sealed partial class VBMath
+    {
+        internal VBMath() { }
+        public static void Randomize() { }
+        public static void Randomize(double Number) { }
+        public static float Rnd() { throw null; }
+        public static float Rnd(float Number) { throw null; }
+    }
 }
 namespace Microsoft.VisualBasic.ApplicationServices
 {
index 14e1162..e27002a 100644 (file)
@@ -57,6 +57,7 @@
     <Compile Include="Microsoft\VisualBasic\VariantType.vb" />
     <Compile Include="Microsoft\VisualBasic\VBFixedArrayAttribute.vb" />
     <Compile Include="Microsoft\VisualBasic\VBFixedStringAttribute.vb" />
+    <Compile Include="Microsoft\VisualBasic\VBMath.vb" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetGroup)' == 'netstandard1.3'">
     <Reference Include="System.Runtime" />
index 10f225f..d81f9d0 100644 (file)
@@ -6,6 +6,12 @@ Namespace Global.Microsoft.VisualBasic.CompilerServices
     <Global.System.Diagnostics.DebuggerNonUserCode()>
     <Global.System.ComponentModel.EditorBrowsable(Global.System.ComponentModel.EditorBrowsableState.Never)>
     Public NotInheritable Class ProjectData
+
+        Friend m_rndSeed As Integer = &H50000I
+
+        'm_oProject is per-Thread
+        <System.ThreadStaticAttribute()> Private Shared m_oProject As ProjectData
+
         Private Sub New()
         End Sub
         Public Overloads Shared Sub SetProjectError(ex As Global.System.Exception)
@@ -14,5 +20,19 @@ Namespace Global.Microsoft.VisualBasic.CompilerServices
         End Sub
         Public Shared Sub ClearProjectError()
         End Sub
+
+        Friend Shared Function GetProjectData() As ProjectData
+            '*************************
+            '*** PERFORMANCE NOTE: ***
+            '*************************
+            ' m_oProject is <ThreadStatic>
+            ' and is pretty expensive to access so we cache to a local
+            ' to cut the number of accesses in half
+            GetProjectData = m_oProject
+            If GetProjectData Is Nothing Then
+                GetProjectData = New ProjectData
+                m_oProject = GetProjectData
+            End If
+        End Function
     End Class
 End Namespace
diff --git a/src/libraries/Microsoft.VisualBasic/src/Microsoft/VisualBasic/VBMath.vb b/src/libraries/Microsoft.VisualBasic/src/Microsoft/VisualBasic/VBMath.vb
new file mode 100644 (file)
index 0000000..33e4095
--- /dev/null
@@ -0,0 +1,113 @@
+' Licensed to the .NET Foundation under one or more agreements.
+' The .NET Foundation licenses this file to you under the MIT license.
+' See the LICENSE file in the project root for more information.
+
+Imports System
+Imports System.Security
+Imports System.Security.Permissions
+Imports System.Text
+Imports System.Globalization
+Imports Microsoft.VisualBasic.CompilerServices
+
+Namespace Microsoft.VisualBasic
+
+    Public Module VBMath
+
+        ' Equivalent to calling VB6 rtRandomNext(1.0)
+        Public Function Rnd() As Single
+            Return Rnd(CSng(1))
+        End Function
+
+        ' Equivalent to VB6 rtRandomNext function
+        Public Function Rnd(ByVal Number As Single) As Single
+            Dim oProj As ProjectData = ProjectData.GetProjectData()
+            Dim rndSeed As Integer = oProj.m_rndSeed
+
+            '  if parameter is zero, generate float from present seed
+            If (Number <> 0.0) Then
+                '  if parameter is negative, use to create new seed
+                If (Number < 0.0) Then
+                    'Original C++ code
+                    'rndSeed = *(ULONG *) & fltVal;
+                    'rndSeed = (rndSeed + (rndSeed >> 24)) & 0xffffffL;
+
+                    rndSeed = BitConverter.ToInt32(BitConverter.GetBytes(Number), 0)
+
+                    Dim i64 As Int64 = rndSeed
+                    i64 = (i64 And &HFFFFFFFFL)
+                    rndSeed = CInt((i64 + (i64 >> 24)) And &HFFFFFFI)
+                End If
+
+                '  if parameter is non-zero, generate a new seed
+                rndSeed = CInt((CLng(rndSeed) * &H43FD43FDL + &HC39EC3L) And &HFFFFFFL)
+            End If
+
+            '  copy back seed value to per-project structure
+            oProj.m_rndSeed = rndSeed
+
+            '  normalize seed to floating value from 0.0 up to 1.0
+            Return CSng(rndSeed) / CSng(16777216.0)
+        End Function
+
+        'Equivalent to RandomizeTimer in the VB6 codebase
+        Public Sub Randomize()
+            Dim oProj As ProjectData = ProjectData.GetProjectData()
+            Dim sngTimer As Single = GetTimer()
+            Dim rndSeed As Int32 = oProj.m_rndSeed
+            Dim lValue As Int32
+
+            '  treat Single as a long Integer
+            lValue = BitConverter.ToInt32(BitConverter.GetBytes(sngTimer), 0)
+
+            '  xor the upper and lower words of the long and put in
+            '  the middle two bytes
+            lValue = ((lValue And &HFFFFI) Xor (lValue >> 16)) << 8
+
+            '  replace the middle two bytes of the seed with lValue
+            rndSeed = (rndSeed And &HFF0000FFI) Or lValue
+
+            '  copy back seed value to per-project structure
+            oProj.m_rndSeed = rndSeed
+        End Sub
+
+        'Equivalent to RandomizeValue in the VB6 codebase
+        Public Sub Randomize(ByVal Number As Double)
+            Dim rndSeed As Integer
+            Dim lValue As Integer
+            Dim oProj As ProjectData
+
+            oProj = ProjectData.GetProjectData()
+            rndSeed = oProj.m_rndSeed
+
+            '  for little-endian R8, the high-order Integer is second half
+            If BitConverter.IsLittleEndian Then
+                lValue = BitConverter.ToInt32(BitConverter.GetBytes(Number), 4)
+            Else
+                lValue = BitConverter.ToInt32(BitConverter.GetBytes(Number), 0)
+            End If
+
+            '  xor the upper and lower words of the Integer and put in
+            '  the middle two bytes
+            ' Original C++ line
+            ' lValue = ((lValue & 0xffff) ^ (lValue >> 16)) << 8;
+            lValue = ((lValue And &HFFFFI) Xor (lValue >> 16)) << 8
+
+            '  replace the middle two bytes of the seed with lValue
+            'Original C++ line
+            ' rndSeed = (rndSeed & 0xff0000ff) | lValue;
+            rndSeed = (rndSeed And &HFF0000FFI) Or lValue
+
+            '  copy back seed value to per-project structure
+            oProj.m_rndSeed = rndSeed
+        End Sub
+
+        Private Function GetTimer() As Single
+            Dim dt As Date
+
+            dt = System.DateTime.Now
+            Return CSng((60 * dt.Hour + dt.Minute) * 60 + dt.Second + (dt.Millisecond / 1000))
+        End Function
+
+    End Module
+
+End Namespace
index 01f314d..77f073d 100644 (file)
@@ -26,6 +26,7 @@
     <Compile Include="DateAndTimeTests.cs" />
     <Compile Include="InformationTests.cs" />
     <Compile Include="UtilsTests.cs" />
+    <Compile Include="VBMathTests.cs" />
     <Compile Include="StringsTests.cs" />
   </ItemGroup>
   <ItemGroup>
diff --git a/src/libraries/Microsoft.VisualBasic/tests/VBMathTests.cs b/src/libraries/Microsoft.VisualBasic/tests/VBMathTests.cs
new file mode 100644 (file)
index 0000000..4a0f773
--- /dev/null
@@ -0,0 +1,190 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.VisualBasic.Tests
+{
+    public class VBMathTests
+    {
+        // Reset seed, without access to implementation internals.
+        private void ResetSeed()
+        {
+            float x = BitConverter.ToSingle(BitConverter.GetBytes(0x8076F5C1), 0);
+            VBMath.Rnd(x);
+
+            float startupSeed = (float)0x50000;
+            float period = 16777216.0f;
+            float currentSeed = VBMath.Rnd(0.0f) * period;
+            Assert.Equal(startupSeed, currentSeed);
+        }
+
+        [Fact]
+        public void Rnd_0_RepeatsPreviousNumber()
+        {
+            ResetSeed();
+
+            float expected;
+            float actual;
+            foreach (var i in Enumerable.Range(0, 5))
+            {
+                expected = VBMath.Rnd();
+                actual = VBMath.Rnd(0.0f);
+                Assert.Equal(expected, actual);
+                actual = VBMath.Rnd(0.0f);
+                Assert.Equal(expected, actual);
+                actual = VBMath.Rnd(0.0f);
+                Assert.Equal(expected, actual);
+            }
+        }
+
+        [Fact]
+        public void Rnd_0_PreviousNumberInSequenceIsNotAlwaysThePreviouslyGeneratedNumber()
+        {
+            ResetSeed();
+
+            float previouslyGeneratedNumber = VBMath.Rnd();
+            VBMath.Randomize(42.0f);
+            float actual = VBMath.Rnd(0.0f);
+            float expected = 0.251064479f;
+            Assert.Equal(expected, actual);
+            Assert.NotEqual(previouslyGeneratedNumber, actual);
+        }
+
+        [Theory]
+        [InlineData(0.5f)]
+        [InlineData(1.0f)]
+        [InlineData(824000.34f)]
+        public void Rnd_Positive_EqualsRndUnit(float positive)
+        {
+            ResetSeed();
+            float actual = VBMath.Rnd(positive);
+            ResetSeed();
+            float expected = VBMath.Rnd();
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void Rnd_Unit_ReturnsExpectedSequence()
+        {
+            ResetSeed();
+
+            float actual1 = VBMath.Rnd();
+            float expected1 = 0.7055475f;
+            Assert.Equal(expected1, actual1);
+
+            float actual2 = VBMath.Rnd();
+            float expected2 = 0.533424f;
+            Assert.Equal(expected2, actual2);
+
+            float actual3 = VBMath.Rnd();
+            float expected3 = 0.5795186f;
+            Assert.Equal(expected3, actual3);
+
+            float actual4 = VBMath.Rnd();
+            float expected4 = 0.289562464f;
+            Assert.Equal(expected4, actual4);
+
+            float actual5 = VBMath.Rnd();
+            float expected5 = 0.301948f;
+            Assert.Equal(expected5, actual5);
+        }
+
+        [Theory]
+        [InlineData(-25820.53f)]
+        [InlineData(-66.0f)]
+        [InlineData(-2.00008f)]
+        public void Rnd_Negative_DoesNotDependUponPreviousState(float number)
+        {
+            ResetSeed();
+
+            float actual1 = VBMath.Rnd(number);
+            VBMath.Rnd();
+            float actual2 = VBMath.Rnd(number);
+            VBMath.Rnd();
+            VBMath.Rnd();
+            float actual3 = VBMath.Rnd(number);
+            VBMath.Rnd();
+            VBMath.Rnd();
+            VBMath.Rnd();
+            VBMath.Rnd();
+            float actual4 = VBMath.Rnd(number);
+            Assert.Equal(actual2, actual1);
+            Assert.Equal(actual3, actual1);
+            Assert.Equal(actual4, actual1);
+        }
+
+        [Theory]
+        [InlineData(-1.2345E38f, 0.0319542289f)]
+        [InlineData(-8000000000.0f, 0.7537669f)]
+        [InlineData(-79.4f, 0.828889251f)]
+        [InlineData(-44.48306f, 0.5418172f)]
+        public void Rnd_Negative_ReturnsExpected(float input, float expected)
+        {
+            ResetSeed();
+            float actual = VBMath.Rnd(input);
+            Assert.Equal(expected, actual);
+        }
+
+        public static IEnumerable<object[]> Randomize_TestData()
+        {
+            yield return new object[] { -44.8 };
+            yield return new object[] { 68.3E10 };
+            yield return new object[] { 45782930.2389523 };
+            yield return new object[] { 32452834095829304624.435 };
+        }
+
+        [Theory]
+        [MemberData(nameof(Randomize_TestData))]
+        public void Randomize_IsIdempotent(double seed)
+        {
+            ResetSeed();
+
+            VBMath.Randomize(seed);
+            float actualState1 = VBMath.Rnd(0.0f);
+            VBMath.Randomize(seed);
+            float actualState2 = VBMath.Rnd(0.0f);
+            Assert.Equal(actualState1, actualState2);
+        }
+
+        [Theory]
+        [MemberData(nameof(Randomize_TestData))]
+        public void Randomize_UseExistingStateWhenGeneratingNewState(double seed)
+        {
+            ResetSeed();
+
+            VBMath.Randomize(seed);
+            float actualState1 = VBMath.Rnd(0.0f);
+
+            VBMath.Rnd();
+            VBMath.Randomize(seed);
+            float actualState2 = VBMath.Rnd(0.0f);
+
+            Assert.NotEqual(actualState1, actualState2);
+        }
+
+        [Fact]
+        public void Randomize_SetsExpectedState()
+        {
+            ResetSeed();
+
+            if (!BitConverter.IsLittleEndian)
+                throw new NotImplementedException("big endian tests");
+
+            VBMath.Randomize(-2E30);
+            Assert.Equal(-0.0297851562f, VBMath.Rnd(0.0f));
+            VBMath.Randomize(-0.003356);
+            Assert.Equal(-0.244613647f, VBMath.Rnd(0.0f));
+            VBMath.Randomize(0.0);
+            Assert.Equal(-1.0f, VBMath.Rnd(0.0f));
+            VBMath.Randomize(10.12345678901);
+            Assert.Equal(-0.503646851f, VBMath.Rnd(0.0f));
+            VBMath.Randomize(3.5356E99);
+            Assert.Equal(-0.462493896f, VBMath.Rnd(0.0f));
+        }
+    }
+}