Attribute VB_Name = "FractionMath"

Option Explicit

Public Type Fraction
    Numerator As Long
    Denominator As Long
End Type

'The length is their denominator
Public Type FractionNormal
    X As Integer
    Y As Integer
    Z As Integer
End Type

Public Type FractionPoint
    X As Fraction
    Y As Fraction
    Z As Fraction
End Type

Public Type FractionBox
    Min As FractionPoint
    Max As FractionPoint
End Type

Public Type FractionPlane
    Point As FractionPoint 'A point intersecting the plane
    Normal As FractionNormal 'The plane's normal
End Type

Public Function MakeFraction(Numerator As Long, Denominator As Long) As Fraction
    MakeFraction.Numerator = Numerator
    MakeFraction.Denominator = Denominator
    If MakeFraction.Denominator < 0 Then
        MakeFraction.Numerator = -MakeFraction.Numerator
        MakeFraction.Denominator = -MakeFraction.Denominator
    End If
End Function

Public Function MakeFraction_Zero() As Fraction
    MakeFraction_Zero = MakeFraction(0, 1)
End Function

Public Function MakeFraction_NaN() As Fraction
    MakeFraction_NaN = MakeFraction(0, 0)
End Function

Public Function MakeFraction_PosInf() As Fraction
    MakeFraction_PosInf = MakeFraction(1, 0)
End Function

Public Function MakeFraction_NegInf() As Fraction
    MakeFraction_NegInf = MakeFraction(-1, 0)
End Function

Public Function IsRealFraction(F As Fraction) As Boolean
    IsRealFraction = (F.Denominator <> 0)
End Function

Public Function MakeFractionNormal_Args(X As Integer, Y As Integer, Z As Integer) As FractionNormal
    MakeFractionNormal_Args.X = X
    MakeFractionNormal_Args.Y = Y
    MakeFractionNormal_Args.Z = Z
End Function

Public Function MakeFractionNormal_Vec(V As Vector3) As FractionNormal
    MakeFractionNormal_Vec.X = V.X
    MakeFractionNormal_Vec.Y = V.Y
    MakeFractionNormal_Vec.Z = V.Z
    MakeFractionNormal_Vec = SimplifyFractionNormal(MakeFractionNormal_Vec)
End Function

Public Function IsRealFractionNormal(N As FractionNormal) As Boolean
    IsRealFractionNormal = (N.X <> 0 Or N.Y <> 0 Or N.Z <> 0)
End Function

Public Function MakeFractionPoint_Flat(XN As Long, XD As Long, YN As Long, YD As Long, ZN As Long, ZD As Long) As FractionPoint
    MakeFractionPoint_Flat = MakeFractionPoint_Args(MakeFraction(XN, XD), MakeFraction(YN, YD), MakeFraction(ZN, ZD))
End Function

Public Function MakeFractionPoint_Args(X As Fraction, Y As Fraction, Z As Fraction) As FractionPoint
    Debug.Assert X.Denominator > 0 And Y.Denominator > 0 And Z.Denominator > 0
    MakeFractionPoint_Args.X = SimplifyFraction(X)
    MakeFractionPoint_Args.Y = SimplifyFraction(Y)
    MakeFractionPoint_Args.Z = SimplifyFraction(Z)
End Function

Public Function MakeFractionPoint_Zero() As FractionPoint
    MakeFractionPoint_Zero.X = MakeFraction_Zero
    MakeFractionPoint_Zero.Y = MakeFraction_Zero
    MakeFractionPoint_Zero.Z = MakeFraction_Zero
End Function

Public Function IsRealFractionPoint(P As FractionPoint) As Boolean
    IsRealFractionPoint = (IsRealFraction(P.X) And IsRealFraction(P.Y) And IsRealFraction(P.Z))
End Function

Public Function MakeFractionBox_Args(Min As FractionPoint, Max As FractionPoint) As FractionBox
    MakeFractionBox_Args.Min = Min
    MakeFractionBox_Args.Max = Max
End Function

Public Function MakeFractionBox_Points(Min As FractionPoint, Max As FractionPoint) As FractionBox
    MakeFractionBox_Points.Min = Min_FractionPoint(Min, Max)
    MakeFractionBox_Points.Max = Max_FractionPoint(Min, Max)
End Function

Public Function MakeFractionBox_Uniform(Radius As Long) As FractionBox
    MakeFractionBox_Uniform = MakeFractionBox_Args(MakeFractionPoint_Flat(-Radius, 1, -Radius, 1, -Radius, 1), MakeFractionPoint_Flat(Radius, 1, Radius, 1, Radius, 1))
End Function

Public Function MakeFractionBox_Big() As FractionBox
    MakeFractionBox_Big = MakeFractionBox_Uniform(1024)
End Function

Public Function MakeFractionPlane(Point As FractionPoint, Normal As FractionNormal) As FractionPlane
    Debug.Assert Point.X.Denominator > 0 And Point.Y.Denominator > 0 And Point.Z.Denominator > 0
    MakeFractionPlane.Point = Point
    MakeFractionPlane.Normal = Normal
End Function

Public Function MakeFractionPlane_Elements(PXN As Long, PXD As Long, PYN As Long, PYD As Long, PZN As Long, PZD As Long, NX As Integer, NY As Integer, NZ As Integer) As FractionPlane
    MakeFractionPlane_Elements = MakeFractionPlane(MakeFractionPoint_Flat(PXN, PXD, PYN, PYD, PZN, PZD), MakeFractionNormal_Args(NX, NY, NZ))
End Function

Public Function IsRealFractionPlane(P As FractionPlane) As Boolean
    IsRealFractionPlane = IsRealFractionNormal(P.Normal) And IsRealFractionPoint(P.Point)
End Function

Public Function ApproximateFraction(F As Fraction) As Double
    Debug.Assert F.Denominator >= 0
    If F.Denominator = 0 Then
        If F.Numerator > 0 Then
            ApproximateFraction = 1E+200 'Approximate positive infinity with a very high value
        ElseIf F.Numerator < 0 Then
            ApproximateFraction = -1E+200 'Approximate negative infinity with a very low value
        Else
            ApproximateFraction = 0 'NaN is not allowed in VB6
        End If
    Else
        ApproximateFraction = F.Numerator / F.Denominator
    End If
End Function

Public Function ApproximateFractionNormal(F As FractionNormal) As Vector3
    ApproximateFractionNormal = NormalVector3(MakeVector3(F.X, F.Y, F.Z))
End Function

Public Function ApproximateFractionPoint(F As FractionPoint) As Vector3
    ApproximateFractionPoint.X = ApproximateFraction(F.X)
    ApproximateFractionPoint.Y = ApproximateFraction(F.Y)
    ApproximateFractionPoint.Z = ApproximateFraction(F.Z)
End Function

Public Function ApproximateFractionPlane(F As FractionPlane) As Plane
    ApproximateFractionPlane.Normal = ApproximateFractionNormal(F.Normal)
    ApproximateFractionPlane.Point = ApproximateFractionPoint(F.Point)
End Function

Public Function ApproximateFractionBox(F As FractionBox) As Box
    ApproximateFractionBox.Min = ApproximateFractionPoint(F.Min)
    ApproximateFractionBox.Max = ApproximateFractionPoint(F.Max)
End Function

Public Function NegFractionNormal(F As FractionNormal) As FractionNormal
    NegFractionNormal.X = -F.X
    NegFractionNormal.Y = -F.Y
    NegFractionNormal.Z = -F.Z
End Function

Public Function FlipFractionPlane(F As FractionPlane) As FractionPlane
    FlipFractionPlane.Normal = NegFractionNormal(F.Normal)
    FlipFractionPlane.Point = F.Point
End Function

Public Function MirrorFractionPlane(P As FractionPlane, Center As FractionPoint, Dimension As Integer) As FractionPlane
    MirrorFractionPlane = P
    Select Case Dimension
    Case 0
        MirrorFractionPlane.Point.X = AddFraction(SubFraction(Center.X, P.Point.X), Center.X)
        MirrorFractionPlane.Normal.X = -P.Normal.X
    Case 1
        MirrorFractionPlane.Point.Y = AddFraction(SubFraction(Center.Y, P.Point.Y), Center.Y)
        MirrorFractionPlane.Normal.Y = -P.Normal.Y
    Case Else
        MirrorFractionPlane.Point.Z = AddFraction(SubFraction(Center.Z, P.Point.Z), Center.Z)
        MirrorFractionPlane.Normal.Z = -P.Normal.Z
    End Select
End Function

'N is not represented as normalized since fractions cannot handle square roots
Public Function FractionDotProduct(P As FractionPoint, N As FractionNormal) As Fraction
    FractionDotProduct = AddFraction(AddFraction(MulFraction(P.X, MakeFraction((N.X), 1)), MulFraction(P.Y, MakeFraction((N.Y), 1))), MulFraction(P.Z, MakeFraction((N.Z), 1)))
End Function

'The result is scaled by the length of the plane normal to fit into fractions.
Public Function FractionPointToPlaneDistance(FPoint As FractionPoint, FPlane As FractionPlane) As Fraction
    FractionPointToPlaneDistance = FractionDotProduct(SubFractionPoint(FPoint, FPlane.Point), FPlane.Normal)
End Function

'Returns 1 if FPoint is in front of FPlane point, -1 if behind and 0 if exactly on
Public Function FractionPointOnPlane(FPoint As FractionPoint, FPlane As FractionPlane) As Integer
    FractionPointOnPlane = CompareLong(FractionPointToPlaneDistance(FPoint, FPlane).Numerator, 0)
End Function

Public Function SimplifyFraction(F As Fraction) As Fraction
    If F.Numerator = 0 Then
        SimplifyFraction = MakeFraction_Zero
    Else
        Static Denominator As Long
        Denominator = GCD(F.Numerator, F.Denominator)
        SimplifyFraction = MakeFraction((F.Numerator * Sgn(F.Denominator)) / Denominator, Abs(F.Denominator) / Denominator)
    End If
End Function

Public Function TruncateFraction(F As Fraction) As Fraction
    TruncateFraction = SimplifyFraction(F)
    Do While TruncateFraction.Denominator > 46300 Or TruncateFraction.Numerator > 46300 Or TruncateFraction.Numerator < -46300
        TruncateFraction.Numerator = TruncateFraction.Numerator \ 2
        TruncateFraction.Denominator = Max_Long(1, TruncateFraction.Denominator \ 2)
        Debug.Print "Truncated " & FractionToString(F) & " to " & FractionToString(TruncateFraction)
    Loop
    Debug.Assert Not (TruncateFraction.Denominator > 46300 Or TruncateFraction.Numerator > 46300 Or TruncateFraction.Numerator < -46300)
End Function

Public Function SimplifyFractionPoint(F As FractionPoint) As FractionPoint
    SimplifyFractionPoint.X = SimplifyFraction(F.X)
    SimplifyFractionPoint.Y = SimplifyFraction(F.Y)
    SimplifyFractionPoint.Z = SimplifyFraction(F.Z)
End Function

Public Function SimplifyFractionNormal(F As FractionNormal) As FractionNormal
    Dim D As Long
    If F.X = 0 And F.Y = 0 And F.Z = 0 Then
        SimplifyFractionNormal = MakeFractionNormal_Args(0, 0, 0) 'Undefined
    ElseIf F.Y = 0 And F.Z = 0 Then
        SimplifyFractionNormal = MakeFractionNormal_Args(Sgn(F.X), 0, 0)
    ElseIf F.X = 0 And F.Z = 0 Then
        SimplifyFractionNormal = MakeFractionNormal_Args(0, Sgn(F.Y), 0)
    ElseIf F.X = 0 And F.Y = 0 Then
        SimplifyFractionNormal = MakeFractionNormal_Args(0, 0, Sgn(F.Z))
    ElseIf F.X = 0 Then
        D = GCD((F.Y), (F.Z))
        SimplifyFractionNormal = MakeFractionNormal_Args(0, F.Y / D, F.Z / D)
    ElseIf F.Y = 0 Then
        D = GCD((F.X), (F.Z))
        SimplifyFractionNormal = MakeFractionNormal_Args(F.X / D, 0, F.Z / D)
    ElseIf F.Z = 0 Then
        D = GCD((F.X), (F.Y))
        SimplifyFractionNormal = MakeFractionNormal_Args(F.X / D, F.Y / D, 0)
    ElseIf Abs(F.X) = Abs(F.Y) And Abs(F.X) = Abs(F.Z) Then
        D = Abs(F.X)
        SimplifyFractionNormal = MakeFractionNormal_Args(F.X / D, F.Y / D, F.Z / D)
    Else
        SimplifyFractionNormal = F
    End If
End Function

Public Function TruncateFractionNormal(F As FractionNormal) As FractionNormal
    Dim Length As Double
    Length = AbsVector3(MakeVector3(F.X, F.Y, F.Z))
    If Length > 46300 Then
        Dim Divisor As Double
        Divisor = Length / 46300
        TruncateFractionNormal = MakeFractionNormal_Vec(DivVector3(MakeVector3(F.X, F.Y, F.Z), Divisor))
    Else
        TruncateFractionNormal = F
    End If
End Function

Public Function LongToString(ByVal L As Long) As String
    Dim Signed As Boolean
    Signed = (L < 0)
    L = Abs(L)
    If L = 0 Then
        LongToString = "0"
    Else
        Do Until L = 0
            LongToString = Chr(Asc("0") + (L Mod 10)) & LongToString
            L = L \ 10
        Loop
    End If
    If Signed Then
        LongToString = "-" & LongToString
    End If
End Function

Public Function FractionToString(F As Fraction) As String
    If F.Denominator = 0 Then
        If F.Numerator > 0 Then
            FractionToString = "Inf"
        ElseIf F.Numerator < 0 Then
            FractionToString = "-Inf"
        Else
            FractionToString = "NaN"
        End If
    ElseIf F.Denominator = 1 Then
        FractionToString = LongToString(F.Numerator)
    Else
        FractionToString = LongToString(F.Numerator) & "/" & LongToString(F.Denominator)
    End If
End Function

Public Function FractionPointToString(F As FractionPoint) As String
    FractionPointToString = FractionToString(F.X) & "," & FractionToString(F.Y) & "," & FractionToString(F.Z)
End Function

Public Function FractionNormalToString(F As FractionNormal) As String
    FractionNormalToString = LongToString(F.X) & "," & LongToString(F.Y) & "," & LongToString(F.Z)
End Function

Public Function FractionBoxToString(F As FractionBox) As String
    FractionBoxToString = FractionPointToString(F.Min) & "," & FractionPointToString(F.Max)
End Function

Public Function FractionPlaneToString(F As FractionPlane) As String
    FractionPlaneToString = FractionPointToString(F.Point) & "," & LongToString(F.Normal.X) & "," & LongToString(F.Normal.Y) & "," & LongToString(F.Normal.Z)
End Function

Public Function LongFromString(Content As String) As Long
    Dim I As Long
    Dim CharCode As Integer
    Dim Signed As Boolean
    Signed = False
    For I = 1 To Len(Content)
        CharCode = Asc(Mid(Content, I, 1))
        If CharCode = Asc("-") Then
            Signed = Not Signed
        ElseIf CharCode >= Asc("0") And CharCode <= Asc("9") Then
            LongFromString = (LongFromString * 10) + (CharCode - Asc("0"))
        End If
    Next I
    If Signed Then
        LongFromString = -LongFromString
    End If
End Function

Public Function FractionFromString(Content As String) As Fraction
    If UCase(Content) = "NAN" Then
        FractionFromString = MakeFraction(0, 0)
    ElseIf UCase(Content) = "INF" Then
        FractionFromString = MakeFraction(1, 0)
    ElseIf UCase(Content) = "-INF" Then
        FractionFromString = MakeFraction(-1, 0)
    Else
        Dim Separator As Long
        Separator = InStr(1, Content, "/", vbTextCompare)
        If Separator > 0 Then
            FractionFromString = MakeFraction(LongFromString(Mid$(Content, 1, Separator - 1)), LongFromString(Mid$(Content, Separator + 1, Len(Content) - Separator)))
        Else
            FractionFromString = MakeFraction(LongFromString(Content), 1)
        End If
    End If
End Function

Public Function FractionPlaneFromString(Content As String) As FractionPlane
    Dim Location As Long
    Dim Separator As Long
    Dim ArgumentIndex As Long
    Dim Argument As String
    Location = 1
    For ArgumentIndex = 0 To 5
        Separator = InStr(Location + 1, Content, ",", vbTextCompare)
        If Separator = 0 Then
            Argument = Mid$(Content, Location, Len(Content) - Location + 1)
        Else
            Argument = Mid$(Content, Location, Separator - Location)
        End If
        Select Case ArgumentIndex
        Case 0
            FractionPlaneFromString.Point.X = FractionFromString(Argument)
        Case 1
            FractionPlaneFromString.Point.Y = FractionFromString(Argument)
        Case 2
            FractionPlaneFromString.Point.Z = FractionFromString(Argument)
        Case 3
            FractionPlaneFromString.Normal.X = LongFromString(Argument)
        Case 4
            FractionPlaneFromString.Normal.Y = LongFromString(Argument)
        Case 5
            FractionPlaneFromString.Normal.Z = LongFromString(Argument)
        Case Else
            MsgBox "Too many arguments given to plane.", vbCritical, "Parsing error!"
            FractionPlaneFromString = MakeFractionPlane_Elements(0, 1, 0, 1, 0, 1, 1, 0, 0)
        End Select
        Location = Separator + 1
    Next ArgumentIndex
    Debug.Assert IsRealFractionPoint(FractionPlaneFromString.Point)
    Debug.Assert IsRealFractionNormal(FractionPlaneFromString.Normal)
End Function

Public Function AddFraction(A As Fraction, B As Fraction) As Fraction
    Dim TA As Fraction
    Dim TB As Fraction
    TA = TruncateFraction(A)
    TB = TruncateFraction(B)
    AddFraction = MakeFraction((TA.Numerator * TB.Denominator) + (TB.Numerator * TA.Denominator), TA.Denominator * TB.Denominator)
    AddFraction = TruncateFraction(AddFraction)
End Function

Public Function AddFractionPoint(A As FractionPoint, B As FractionPoint) As FractionPoint
    AddFractionPoint.X = AddFraction(A.X, B.X)
    AddFractionPoint.Y = AddFraction(A.Y, B.Y)
    AddFractionPoint.Z = AddFraction(A.Z, B.Z)
End Function

Public Function SubFraction(A As Fraction, B As Fraction) As Fraction
    Dim TA As Fraction
    Dim TB As Fraction
    TA = TruncateFraction(A)
    TB = TruncateFraction(B)
    SubFraction = MakeFraction((TA.Numerator * TB.Denominator) - (TB.Numerator * TA.Denominator), TA.Denominator * TB.Denominator)
    SubFraction = TruncateFraction(SubFraction)
End Function

Public Function SubFractionPoint(A As FractionPoint, B As FractionPoint) As FractionPoint
    SubFractionPoint.X = SubFraction(A.X, B.X)
    SubFractionPoint.Y = SubFraction(A.Y, B.Y)
    SubFractionPoint.Z = SubFraction(A.Z, B.Z)
End Function

Public Function MulFraction(A As Fraction, B As Fraction) As Fraction
    Dim TA As Fraction
    Dim TB As Fraction
    TA = TruncateFraction(A)
    TB = TruncateFraction(B)
    MulFraction = MakeFraction(TA.Numerator * TB.Numerator, TA.Denominator * TB.Denominator)
    MulFraction = TruncateFraction(MulFraction)
End Function

Public Function DivFraction(A As Fraction, B As Fraction) As Fraction
    Dim TA As Fraction
    Dim TB As Fraction
    TA = TruncateFraction(A)
    TB = TruncateFraction(B)
    DivFraction = MakeFraction(TA.Numerator * TB.Denominator, TA.Denominator * TB.Numerator)
    DivFraction = TruncateFraction(DivFraction)
End Function

Public Function MakeFractionBoxFromTwoPoints(A As FractionPoint, B As FractionPoint) As FractionBox
    MakeFractionBoxFromTwoPoints.Min.X = Min_Fraction(A.X, B.X)
    MakeFractionBoxFromTwoPoints.Max.X = Max_Fraction(A.X, B.X)
    MakeFractionBoxFromTwoPoints.Min.Y = Min_Fraction(A.Y, B.Y)
    MakeFractionBoxFromTwoPoints.Max.Y = Max_Fraction(A.Y, B.Y)
    MakeFractionBoxFromTwoPoints.Min.Z = Min_Fraction(A.Z, B.Z)
    MakeFractionBoxFromTwoPoints.Max.Z = Max_Fraction(A.Z, B.Z)
End Function

Private Function CompareLong(A As Long, B As Long) As Integer
    If A > B Then
        CompareLong = 1 'Greater
    ElseIf A < B Then
        CompareLong = -1 'Lesser
    Else
        CompareLong = 0 'Equal
    End If
End Function

'Returns 1 if A > B, 0 if A = B, -1 if A < B and -2 if undecided
Private Function CompareFraction(A As Fraction, B As Fraction) As Integer
    On Error GoTo OverFlow
    If (A.Numerator = 0 And A.Denominator = 0) Or (B.Numerator = 0 And B.Denominator = 0) Then
        'One input is not a number
        CompareFraction = -2 'Inconclusive
    ElseIf A.Denominator = 0 And B.Denominator = 0 Then
        'Both are infinity
        CompareFraction = CompareLong(A.Numerator, B.Numerator)
        If CompareFraction = 0 Then CompareFraction = -2 'Infinite comparison is inconclusive on the same side
    ElseIf A.Denominator = 0 Then
        'A is infinity
        CompareFraction = CompareLong(A.Numerator, 0)
    ElseIf B.Denominator = 0 Then
        'B is infinity
        CompareFraction = CompareLong(0, B.Numerator)
    ElseIf A.Denominator = B.Denominator Then
        CompareFraction = CompareLong(A.Numerator, B.Numerator)
    Else
        'A and B are real fractions
        Dim CommonDenominator As Long
        Dim AM As Long
        Dim BM As Long
        CommonDenominator = GCD(A.Denominator, B.Denominator)
        AM = A.Numerator * (B.Denominator / CommonDenominator)
        BM = B.Numerator * (A.Denominator / CommonDenominator)
        CompareFraction = CompareLong(AM, BM)
    End If
    Exit Function
OverFlow:
    CompareFraction = -2 'Overflow is undecided
End Function

Public Function FractionIsGreater(A As Fraction, B As Fraction) As Boolean
    FractionIsGreater = CompareFraction(A, B) = 1
End Function

Public Function FractionIsLesser(A As Fraction, B As Fraction) As Boolean
    FractionIsLesser = CompareFraction(A, B) = -1
End Function

Public Function FractionIsEqual(A As Fraction, B As Fraction) As Boolean
    FractionIsEqual = CompareFraction(A, B) = 0
End Function

Public Function Min_Fraction(A As Fraction, B As Fraction) As Fraction
    If FractionIsLesser(A, B) Then
        Min_Fraction = A
    Else
        Min_Fraction = B
    End If
End Function

Public Function Max_Fraction(A As Fraction, B As Fraction) As Fraction
    If FractionIsGreater(A, B) Then
        Max_Fraction = A
    Else
        Max_Fraction = B
    End If
End Function

Public Function Lerp_Fraction(A As Fraction, B As Fraction, Ratio As Fraction) As Fraction
    Lerp_Fraction = AddFraction(MulFraction(B, Ratio), MulFraction(A, SubFraction(MakeFraction(1, 1), Ratio)))
End Function

Public Function LerpFractionPoint(A As FractionPoint, B As FractionPoint, Ratio As Fraction) As FractionPoint
    LerpFractionPoint.X = Lerp_Fraction(A.X, B.X, Ratio)
    LerpFractionPoint.Y = Lerp_Fraction(A.Y, B.Y, Ratio)
    LerpFractionPoint.Z = Lerp_Fraction(A.Z, B.Z, Ratio)
End Function

Public Function Min_FractionPoint(A As FractionPoint, B As FractionPoint) As FractionPoint
    Min_FractionPoint.X = Min_Fraction(A.X, B.X)
    Min_FractionPoint.Y = Min_Fraction(A.Y, B.Y)
    Min_FractionPoint.Z = Min_Fraction(A.Z, B.Z)
End Function

Public Function Max_FractionPoint(A As FractionPoint, B As FractionPoint) As FractionPoint
    Max_FractionPoint.X = Max_Fraction(A.X, B.X)
    Max_FractionPoint.Y = Max_Fraction(A.Y, B.Y)
    Max_FractionPoint.Z = Max_Fraction(A.Z, B.Z)
End Function

Public Function ExtendFractionBoxUsingPoint(B As FractionBox, P As FractionPoint) As FractionBox
    ExtendFractionBoxUsingPoint.Min = Min_FractionPoint(B.Min, P)
    ExtendFractionBoxUsingPoint.Max = Max_FractionPoint(B.Max, P)
End Function

'Pre-condition: A <> 0 and B <> 0
'Post-condition: The greatest common denominator for A and B
Public Function GCD(A As Long, B As Long) As Long
    Dim I As Long
    Dim J As Long
    Dim Remainder As Long
    Dim PreviousRemainder As Long
    I = Abs(A)
    J = Abs(B)
    Debug.Assert I > 0 And J > 0 'Abort if one of the numbers are 0
    Remainder = J
    Do Until Remainder = 0 'Use the Euclidean algorithm
        PreviousRemainder = Remainder
        Remainder = I Mod J
        I = J
        J = Remainder
    Loop
    GCD = Abs(PreviousRemainder)
End Function

Public Sub RegressionTest_Fraction()
    'Serialization
    Debug.Assert LongToString(-286456) = "-286456"
    Debug.Assert LongToString(-10) = "-10"
    Debug.Assert LongToString(-9) = "-9"
    Debug.Assert LongToString(-1) = "-1"
    Debug.Assert LongToString(0) = "0"
    Debug.Assert LongToString(1) = "1"
    Debug.Assert LongToString(9) = "9"
    Debug.Assert LongToString(10) = "10"
    Debug.Assert LongToString(11) = "11"
    Debug.Assert LongToString(99) = "99"
    Debug.Assert LongToString(100) = "100"
    Debug.Assert LongToString(74517) = "74517"
    Debug.Assert LongFromString("-91275") = -91275
    Debug.Assert LongFromString("-11") = -11
    Debug.Assert LongFromString("-10") = -10
    Debug.Assert LongFromString("-9") = -9
    Debug.Assert LongFromString("-1") = -1
    Debug.Assert LongFromString("0") = 0
    Debug.Assert LongFromString("1") = 1
    Debug.Assert LongFromString("9") = 9
    Debug.Assert LongFromString("10") = 10
    Debug.Assert LongFromString("624") = 624
    Debug.Assert LongFromString("72485") = 72485
    Debug.Assert FractionToString(MakeFraction(1, 3)) = "1/3"
    Debug.Assert FractionToString(MakeFraction(2, 6)) = "2/6"
    Debug.Assert FractionNormalToString(MakeFractionNormal_Args(32, 0, 8)) = "32,0,8"
    Debug.Assert FractionPlaneToString(MakeFractionPlane_Elements(1, 7, -3, 4, 7, 12, 2, 5, 7)) = "1/7,-3/4,7/12,2,5,7"
    Debug.Assert FractionToString(FractionFromString("-8/65")) = "-8/65"
    Debug.Assert FractionToString(FractionFromString("7 /64")) = "7/64"
    Debug.Assert FractionToString(FractionFromString("40/ -37")) = "-40/37"
    Debug.Assert FractionToString(FractionFromString("12 / 725")) = "12/725"
    
    'Simplification
    Debug.Assert FractionToString(SimplifyFraction(MakeFraction(2, 6))) = "1/3"
    Debug.Assert FractionNormalToString(SimplifyFractionNormal(MakeFractionNormal_Args(32, 0, 8))) = "4,0,1"
    
    'Comparison
    Debug.Assert CompareFraction(MakeFraction(1, 9), MakeFraction(2, 19)) = 1
    Debug.Assert CompareFraction(MakeFraction(1, 9), MakeFraction(2, 18)) = 0
    Debug.Assert CompareFraction(MakeFraction(1, 9), MakeFraction(2, 17)) = -1
    
    'Side of plane
    Debug.Assert FractionPointOnPlane(MakeFractionPoint_Flat(1, 1, 0, 1, 0, 1), MakeFractionPlane(MakeFractionPoint_Flat(0, 1, 0, 1, 0, 1), MakeFractionNormal_Args(1, 0, 0))) = 1
    Debug.Assert FractionPointOnPlane(MakeFractionPoint_Flat(0, 1, 0, 1, 0, 1), MakeFractionPlane(MakeFractionPoint_Flat(0, 1, 0, 1, 0, 1), MakeFractionNormal_Args(1, 0, 0))) = 0
    Debug.Assert FractionPointOnPlane(MakeFractionPoint_Flat(-1, 1, 0, 1, 0, 1), MakeFractionPlane(MakeFractionPoint_Flat(0, 1, 0, 1, 0, 1), MakeFractionNormal_Args(1, 0, 0))) = -1
    Debug.Assert FractionPointOnPlane(MakeFractionPoint_Flat(2, 3, 0, 1, 0, 1), MakeFractionPlane(MakeFractionPoint_Flat(3, 5, 0, 1, 0, 1), MakeFractionNormal_Args(-1, 0, 0))) = -1
    Debug.Assert FractionPointOnPlane(MakeFractionPoint_Flat(8, 4, 0, 1, 0, 1), MakeFractionPlane(MakeFractionPoint_Flat(2, 1, 0, 1, 0, 1), MakeFractionNormal_Args(15, 23, -75))) = 0
    Debug.Assert FractionPointOnPlane(MakeFractionPoint_Flat(19, 1, -19, 1, 463, 86), MakeFractionPlane(MakeFractionPoint_Flat(0, 1, 0, 1, 0, 1), MakeFractionNormal_Args(1, 1, 0))) = 0
    Debug.Assert FractionPointOnPlane(MakeFractionPoint_Flat(12, 1, -8, 1, 463, 86), MakeFractionPlane(MakeFractionPoint_Flat(0, 1, 0, 1, 0, 1), MakeFractionNormal_Args(2, 3, 0))) = 0
End Sub
