Changed return type and parameter type of compiler support functions back to usual because of complications when compiling compile time codeblocks and comparing field values with literals

//
// Copyright (c) XSharp B.V.  All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//
// functions used by the compiler

// <summary>
// Compare 2 strings. This function is used by the compiler for string comparisons
// </summary>
// <param name="strLHS">The first string .</param>
// <param name="strRHS">The second string.</param>
// <returns>
// -1 strLHS precedes strRHS in the sort order.
//  0 strLHS occurs in the same position as strRHS in the sort order.
//  1 strLHS follows strRHS in the sort order.
// Note this this function respects SetCollation() and SetExact()
// </returns>
/// <exclude />
FUNCTION __StringCompare(strLHS AS STRING, strRHS AS STRING) AS INT
    LOCAL ret AS INT
    // Only when vo13 is off and SetExact = TRUE
    IF !RuntimeState.CompilerOptionVO13 .AND. RuntimeState.Exact
        RETURN String.Compare( strLHS,  strRHS)
    ENDIF
    IF Object.ReferenceEquals(strLHS, strRHS)
        ret := 0
    ELSEIF strLHS == NULL
        IF strRHS == NULL			// null and null are equal
            ret := 0
        ELSE
            ret := -1				// null precedes a string
        ENDIF
    ELSEIF strRHS == NULL			// a string comes after null
        ret := 1
    ELSE							// both not null
        // With Not Exact comparison we only compare the length of the RHS string
        // and we always use the unicode comparison because that is what vulcan does
        // This is done to make sure that >= and <= will also return TRUE when the LHS is longer than the RHS
        // The ordinal comparison can be done here because when both strings start with the same characters
        // then equality is guaranteed regardless of collations or other rules.
        // collations and other rules are really only relevant when both strings are different
        IF  !RuntimeState.Exact
            LOCAL lengthRHS AS INT
            lengthRHS := strRHS:Length
            IF lengthRHS == 0 .OR. lengthRHS <= strLHS:Length  .AND. String.Compare( strLHS, 0, strRHS, 0, lengthRHS , StringComparison.Ordinal ) == 0
                RETURN 0
            ENDIF
        ENDIF
        // either exact or RHS longer than LHS
        VAR mode := RuntimeState.CollationMode
        SWITCH mode
        CASE CollationMode.Windows
            ret := XSharp.StringHelpers.CompareWindows(strLHS, strRHS)
        CASE CollationMode.Clipper
            ret := XSharp.StringHelpers.CompareClipper(strLHS, strRHS)
        CASE CollationMode.Unicode
            ret := String.Compare(strLHS, strRHS)
        OTHERWISE
            ret := String.CompareOrdinal(strLHS, strRHS)
        END SWITCH
    ENDIF
    RETURN ret

    /// <summary>
    /// Compare 2 strings. This function is used by the compiler for string comparisons
    /// </summary>
    /// <param name="strLHS">The first string .</param>
    /// <param name="strRHS">The second string.</param>
    /// <returns>
    /// TRUE when the strings are equal, FALSE when they are not equal
    /// This function respects SetExact()
    /// </returns>
FUNCTION  __StringEquals(strLHS AS STRING, strRHS AS STRING) AS LOGIC
    LOCAL IsEqual:= FALSE AS LOGIC
    LOCAL lengthRHS AS INT
    IF Object.ReferenceEquals(strLHS, strRHS)
        IsEqual := TRUE
    ELSEIF RuntimeState.Exact
        IsEqual := String.Equals(strLHS , strRHS)
    ELSEIF strLHS != NULL .AND. strRHS != NULL
        lengthRHS := strRHS:Length
        IF lengthRHS == 0
            IsEqual := TRUE        // With SetExact(FALSE) then "aaa" = "" returns TRUE
        ELSEIF lengthRHS <= strLHS:Length

            IsEqual := String.Compare( strLHS, 0, strRHS, 0, lengthRHS, StringComparison.Ordinal ) == 0
        ENDIF
    ELSEIF strLHS == NULL .AND. strRHS == NULL
        IsEqual := TRUE
    ENDIF
    RETURN IsEqual

    /// <summary>
    /// Compare 2 strings. This function is used by the compiler for string comparisons
    /// </summary>
    /// <param name="strLHS">The first string .</param>
    /// <param name="strRHS">The second string.</param>
    /// <returns>
    /// TRUE when the strings are not equal, FALSE when they are equal
    /// This function respects SetExact()
    /// </returns>
FUNCTION  __StringNotEquals(strLHS AS STRING, strRHS AS STRING) AS LOGIC
    LOCAL notEquals := FALSE AS LOGIC
    LOCAL lengthRHS AS INT
    IF Object.ReferenceEquals(strLHS, strRHS)
        notEquals := FALSE
    ELSEIF RuntimeState.Exact
        notEquals := !String.Equals(strLHS , strRHS)
    ELSEIF strLHS != NULL .AND. strRHS != NULL
        // shortcut: chec first char
        lengthRHS := strRHS:Length
        IF lengthRHS == 0
            notEquals := FALSE        // With SetExact(FALSE) then "aaa" = "" returns TRUE
        ELSEIF lengthRHS <= strLHS:Length
            notEquals := String.Compare( strLHS, 0, strRHS, 0, lengthRHS, StringComparison.Ordinal ) != 0
        ELSE
            notEquals := TRUE
        ENDIF
    ELSEIF strLHS == NULL .AND. strRHS == NULL
        notEquals := FALSE
    ELSE
        notEquals := TRUE
    ENDIF
    RETURN notEquals

    /// <summary>
    /// Remove leading and trailing spaces from a string.
    /// </summary>
    /// <param name="c">The string to be trimmed.</param>
    /// <returns>
    /// The original string without leading and trailing spaces
    /// </returns>
    // _FIELD->Name

FUNCTION __FieldGet( fieldName AS STRING ) AS USUAL
    LOCAL fieldpos := FieldPos( fieldName ) AS DWORD
    LOCAL ret := NULL AS OBJECT
    IF fieldpos == 0
        THROW Error.VODBError( EG_ARG, EDB_FIELDNAME, __FUNCTION__,  fieldName  )
    ELSE
         _DbThrowErrorOnFailure(__FUNCTION__, CoreDb.FieldGet( fieldpos, REF ret ))
    ENDIF
    RETURN ret

    // CUSTOMER->NAME
/// <exclude/>
FUNCTION __FieldGetWa( alias AS STRING, fieldName AS STRING ) AS USUAL
    LOCAL ret AS OBJECT
    LOCAL newArea := SELECT( alias ) AS DWORD
    LOCAL curArea := RuntimeState.CurrentWorkarea AS DWORD
    IF newArea > 0
        RuntimeState.CurrentWorkarea := newArea
        TRY
            ret := __FieldGet( fieldName )
        FINALLY
            RuntimeState.CurrentWorkarea := curArea
        END TRY
    ELSE
        THROW Error.VODBError( EG_ARG, EDB_BADALIAS, __FUNCTION__, alias  )
    ENDIF
    RETURN ret

    // _FIELD->Name := "Foo"
/// <exclude/>
FUNCTION __FieldSet( fieldName AS STRING, oValue AS USUAL ) AS USUAL
    LOCAL fieldpos := FieldPos( fieldName ) AS DWORD
    IF fieldpos == 0
        THROW Error.VODBError( EG_ARG, EDB_FIELDNAME, __FUNCTION__,  fieldName  )
    ELSE
        _DbThrowErrorOnFailure(__FUNCTION__, CoreDb.FieldPut( fieldpos, oValue))
    ENDIF
    // We return the original value to allow chained expressions
    RETURN oValue

    // CUSTOMER->Name := "Foo"
/// <exclude/>
FUNCTION __FieldSetWa( alias AS STRING, fieldName AS STRING, uValue AS USUAL ) AS USUAL
    LOCAL newArea := SELECT( alias ) AS DWORD
    LOCAL curArea := RuntimeState.CurrentWorkarea AS DWORD
    IF newArea > 0
        RuntimeState.CurrentWorkarea := newArea

        TRY
            __FieldSet( fieldName, uValue )
        FINALLY
            RuntimeState.CurrentWorkarea := curArea
        END TRY
    ELSE
        THROW Error.VODBError( EG_ARG, EDB_BADALIAS, __FUNCTION__, alias  )
    ENDIF
    // Note: must return the same value passed in, to allow chained assignment expressions
    RETURN uValue

    // MEMVAR myName
    // ? MyName
/// <exclude/>
FUNCTION __MemVarGet(cName AS STRING) AS USUAL
    RETURN NIL

    // MEMVAR myName
    // MyName := "NewValue"
/// <exclude/>
FUNCTION __MemVarPut(cName AS STRING, uValue AS USUAL) AS USUAL
    RETURN uValue

/// <exclude/>
FUNCTION __VarGet(cName AS STRING) AS USUAL
    IF FieldPos(cName) > 0
        RETURN __FieldGet(cName)
    ENDIF
    RETURN __MemVarGet(cName)

/// <exclude/>
FUNCTION __VarPut(cName AS STRING, uValue AS USUAL) AS USUAL
    IF FieldPos(cName) > 0
        RETURN __FieldSet(cName, uValue)
    ENDIF
    RETURN __MemVarPut(cName, uValue)

    // ALIAS->(DoSomething())
    // is translated to
    // __pushWorkarea( alias ) ; DoSomething() ; __popWorkArea()

/// <exclude/>
FUNCTION __pushWorkarea( alias AS USUAL ) AS VOID
    LOCAL newArea := SELECT( alias ) AS DWORD
    IF newArea > 0
        RuntimeState.PushCurrentWorkarea( newArea )
    ELSE
        THROW Error.VODBError( EG_ARG, EDB_BADALIAS, { alias } )
    ENDIF
    RETURN

    // This is used by the -> operator to restore the previous workarea
/// <exclude/>
FUNCTION __popWorkarea() AS VOID
    RuntimeState.PopCurrentWorkarea()
    RETURN

//
// Copyright (c) XSharp B.V.  All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//
// functions used by the compiler

// <summary>
// Compare 2 strings. This function is used by the compiler for string comparisons
// </summary>
// <param name="strLHS">The first string .</param>
// <param name="strRHS">The second string.</param>
// <returns>
// -1 strLHS precedes strRHS in the sort order.
//  0 strLHS occurs in the same position as strRHS in the sort order.
//  1 strLHS follows strRHS in the sort order.
// Note this this function respects SetCollation() and SetExact()
// </returns>
/// <exclude />
FUNCTION __StringCompare(strLHS AS STRING, strRHS AS STRING) AS INT
    LOCAL ret AS INT
    // Only when vo13 is off and SetExact = TRUE
    IF !RuntimeState.CompilerOptionVO13 .AND. RuntimeState.Exact
        RETURN String.Compare( strLHS,  strRHS)
    ENDIF
    IF Object.ReferenceEquals(strLHS, strRHS)
        ret := 0
    ELSEIF strLHS == NULL
        IF strRHS == NULL			// null and null are equal
            ret := 0
        ELSE
            ret := -1				// null precedes a string
        ENDIF
    ELSEIF strRHS == NULL			// a string comes after null
        ret := 1
    ELSE							// both not null
        // With Not Exact comparison we only compare the length of the RHS string
        // and we always use the unicode comparison because that is what vulcan does
        // This is done to make sure that >= and <= will also return TRUE when the LHS is longer than the RHS
        // The ordinal comparison can be done here because when both strings start with the same characters
        // then equality is guaranteed regardless of collations or other rules.
        // collations and other rules are really only relevant when both strings are different
        IF  !RuntimeState.Exact
            LOCAL lengthRHS AS INT
            lengthRHS := strRHS:Length
            IF lengthRHS == 0 .OR. lengthRHS <= strLHS:Length  .AND. String.Compare( strLHS, 0, strRHS, 0, lengthRHS , StringComparison.Ordinal ) == 0
                RETURN 0
            ENDIF
        ENDIF
        // either exact or RHS longer than LHS
        VAR mode := RuntimeState.CollationMode
        SWITCH mode
        CASE CollationMode.Windows
            ret := XSharp.StringHelpers.CompareWindows(strLHS, strRHS)
        CASE CollationMode.Clipper
            ret := XSharp.StringHelpers.CompareClipper(strLHS, strRHS)
        CASE CollationMode.Unicode
            ret := String.Compare(strLHS, strRHS)
        OTHERWISE
            ret := String.CompareOrdinal(strLHS, strRHS)
        END SWITCH
    ENDIF
    RETURN ret

    /// <summary>
    /// Compare 2 strings. This function is used by the compiler for string comparisons
    /// </summary>
    /// <param name="strLHS">The first string .</param>
    /// <param name="strRHS">The second string.</param>
    /// <returns>
    /// TRUE when the strings are equal, FALSE when they are not equal
    /// This function respects SetExact()
    /// </returns>
FUNCTION  __StringEquals(strLHS AS STRING, strRHS AS STRING) AS LOGIC
    LOCAL IsEqual:= FALSE AS LOGIC
    LOCAL lengthRHS AS INT
    IF Object.ReferenceEquals(strLHS, strRHS)
        IsEqual := TRUE
    ELSEIF RuntimeState.Exact
        IsEqual := String.Equals(strLHS , strRHS)
    ELSEIF strLHS != NULL .AND. strRHS != NULL
        lengthRHS := strRHS:Length
        IF lengthRHS == 0
            IsEqual := TRUE        // With SetExact(FALSE) then "aaa" = "" returns TRUE
        ELSEIF lengthRHS <= strLHS:Length

            IsEqual := String.Compare( strLHS, 0, strRHS, 0, lengthRHS, StringComparison.Ordinal ) == 0
        ENDIF
    ELSEIF strLHS == NULL .AND. strRHS == NULL
        IsEqual := TRUE
    ENDIF
    RETURN IsEqual

    /// <summary>
    /// Compare 2 strings. This function is used by the compiler for string comparisons
    /// </summary>
    /// <param name="strLHS">The first string .</param>
    /// <param name="strRHS">The second string.</param>
    /// <returns>
    /// TRUE when the strings are not equal, FALSE when they are equal
    /// This function respects SetExact()
    /// </returns>
FUNCTION  __StringNotEquals(strLHS AS STRING, strRHS AS STRING) AS LOGIC
    LOCAL notEquals := FALSE AS LOGIC
    LOCAL lengthRHS AS INT
    IF Object.ReferenceEquals(strLHS, strRHS)
        notEquals := FALSE
    ELSEIF RuntimeState.Exact
        notEquals := !String.Equals(strLHS , strRHS)
    ELSEIF strLHS != NULL .AND. strRHS != NULL
        // shortcut: chec first char
        lengthRHS := strRHS:Length
        IF lengthRHS == 0
            notEquals := FALSE        // With SetExact(FALSE) then "aaa" = "" returns TRUE
        ELSEIF lengthRHS <= strLHS:Length
            notEquals := String.Compare( strLHS, 0, strRHS, 0, lengthRHS, StringComparison.Ordinal ) != 0
        ELSE
            notEquals := TRUE
        ENDIF
    ELSEIF strLHS == NULL .AND. strRHS == NULL
        notEquals := FALSE
    ELSE
        notEquals := TRUE
    ENDIF
    RETURN notEquals

    /// <summary>
    /// Remove leading and trailing spaces from a string.
    /// </summary>
    /// <param name="c">The string to be trimmed.</param>
    /// <returns>
    /// The original string without leading and trailing spaces
    /// </returns>
    // _FIELD->Name

FUNCTION __FieldGet( fieldName AS STRING ) AS OBJECT
    LOCAL fieldpos := FieldPos( fieldName ) AS DWORD
    LOCAL ret := NULL AS OBJECT
    IF fieldpos == 0
        THROW Error.VODBError( EG_ARG, EDB_FIELDNAME, __FUNCTION__,  fieldName  )
    ELSE
         _DbThrowErrorOnFailure(__FUNCTION__, CoreDb.FieldGet( fieldpos, REF ret ))
    ENDIF
    RETURN ret

    // CUSTOMER->NAME
/// <exclude/>
FUNCTION __FieldGetWa( alias AS STRING, fieldName AS STRING ) AS OBJECT
    LOCAL ret AS OBJECT
    LOCAL newArea := SELECT( alias ) AS DWORD
    LOCAL curArea := RuntimeState.CurrentWorkarea AS DWORD
    IF newArea > 0
        RuntimeState.CurrentWorkarea := newArea
        TRY
            ret := __FieldGet( fieldName )
        FINALLY
            RuntimeState.CurrentWorkarea := curArea
        END TRY
    ELSE
        THROW Error.VODBError( EG_ARG, EDB_BADALIAS, __FUNCTION__, alias  )
    ENDIF
    RETURN ret

    // _FIELD->Name := "Foo"
/// <exclude/>
FUNCTION __FieldSet( fieldName AS STRING, oValue AS OBJECT ) AS OBJECT
    LOCAL fieldpos := FieldPos( fieldName ) AS DWORD
    IF fieldpos == 0
        THROW Error.VODBError( EG_ARG, EDB_FIELDNAME, __FUNCTION__,  fieldName  )
    ELSE
        _DbThrowErrorOnFailure(__FUNCTION__, CoreDb.FieldPut( fieldpos, oValue))
    ENDIF
    // We return the original value to allow chained expressions
    RETURN oValue

    // CUSTOMER->Name := "Foo"
/// <exclude/>
FUNCTION __FieldSetWa( alias AS STRING, fieldName AS STRING, uValue AS USUAL ) AS USUAL
    LOCAL newArea := SELECT( alias ) AS DWORD
    LOCAL curArea := RuntimeState.CurrentWorkarea AS DWORD
    IF newArea > 0
        RuntimeState.CurrentWorkarea := newArea

        TRY
            __FieldSet( fieldName, uValue )
        FINALLY
            RuntimeState.CurrentWorkarea := curArea
        END TRY
    ELSE
        THROW Error.VODBError( EG_ARG, EDB_BADALIAS, __FUNCTION__, alias  )
    ENDIF
    // Note: must return the same value passed in, to allow chained assignment expressions
    RETURN uValue

    // MEMVAR myName
    // ? MyName
/// <exclude/>
FUNCTION __MemVarGet(cName AS STRING) AS USUAL
    RETURN NIL

    // MEMVAR myName
    // MyName := "NewValue"
/// <exclude/>
FUNCTION __MemVarPut(cName AS STRING, uValue AS USUAL) AS USUAL
    RETURN uValue

/// <exclude/>
FUNCTION __VarGet(cName AS STRING) AS OBJECT
    IF FieldPos(cName) > 0
        RETURN __FieldGet(cName)
    ENDIF
    RETURN __MemVarGet(cName)

/// <exclude/>
FUNCTION __VarPut(cName AS STRING, uValue AS OBJECT) AS OBJECT
    IF FieldPos(cName) > 0
        RETURN __FieldSet(cName, uValue)
    ENDIF
    RETURN __MemVarPut(cName, uValue)

    // ALIAS->(DoSomething())
    // is translated to
    // __pushWorkarea( alias ) ; DoSomething() ; __popWorkArea()

/// <exclude/>
FUNCTION __pushWorkarea( alias AS USUAL ) AS VOID
    LOCAL newArea := SELECT( alias ) AS DWORD
    IF newArea > 0
        RuntimeState.PushCurrentWorkarea( newArea )
    ELSE
        THROW Error.VODBError( EG_ARG, EDB_BADALIAS, { alias } )
    ENDIF
    RETURN

    // This is used by the -> operator to restore the previous workarea
/// <exclude/>
FUNCTION __popWorkarea() AS VOID
    RuntimeState.PopCurrentWorkarea()
    RETURN


[Runtime tests] Added test for https://github.com/X-Sharp/XSharpPublic/issues/1001

//
// Copyright (c) XSharp B.V.  All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//
USING System
USING System.Collections.Generic
USING System.Linq
USING System.Text
USING XUnit
USING System.Globalization

DEFINE FTRUE  := TRUE
DEFINE FFALSE := FALSE
BEGIN NAMESPACE XSharp.RT.Tests

	CLASS VoConversionTests

		[Fact, Trait("Category", "Conversion")];
		METHOD AsStringTest() AS VOID
			LOCAL u AS USUAL
			u := "123"
			Assert.Equal("123", AsString(u))
			u := #A123
			Assert.Equal("A123", AsString(u))

			Assert.Equal("1", AsString(1))
			VAR c1 := GetRTFullPath()
			Assert.Equal(TRUE, c1:ToLower():IndexOf(".rt.dll") > 0)

			VAR n1 := GetThreadCount()
			Assert.Equal(TRUE, n1 > 1)

			LOCAL p AS PTR
			p := @p
			Assert.Equal(AsString(p), "0x" + AsHexString(p) )
			Assert.Equal(10, AsString(p):Length)
			p := NULL_PTR
			Assert.Equal("0x00000000", AsString(p))
			Assert.Equal("00000000", AsHexString(p))

		[Fact, Trait("Category", "Conversion")];
		METHOD StrTest() AS VOID
			LOCAL c AS STRING
            LOCAL f AS FLOAT
			SetDecimalSep(46)
            SetDecimal(2)
			c := Str3(12.3456,5,2)
			Assert.Equal("12.35", c)	// ROunded up
			c := Str3(12.3411,5,2)
			Assert.Equal("12.34", c)	// ROunded down
            f := FloatFormat(12.3456,10,4)
			c := Str3(f,10,5)
			Assert.Equal("  12.34560", c)	// ROunded down
			c := StrZero(12.3456,10,2)
			Assert.Equal("0000012.35", c)	// ROunded up
			c := Str3(2.49999,4,2)
			Assert.Equal("2.50", c )
			c := Str3(2.50012,4,2)
			Assert.Equal("2.50", c )

			c := Str3(70.00 - 65.01 - 4.99 , 16 , 2)
			Assert.Equal("            0.00", c )
			c := Str3(70.00 - 65.01 - 4.99 , 20 , 15)
			Assert.Equal("  -0.000000000000005", c )

			c := Str3(123456.7890/2.34, 25,15)
			Assert.Equal("    52759.311538461545000", c )
			c := Str(123456.7890/2.34, 25,15)
			Assert.Equal("    52759.311538461545000", c )

			SetDecimalSep(Asc(","))
			f := 1.2345999999
			Assert.Equal("1,2", Str(f, 30,1):Trim() )
			Assert.Equal("1,23", Str(f, 30,2):Trim() )
			Assert.Equal("1,235", Str(f, 30,3):Trim() )
			Assert.Equal("1,2346", Str(f, 30,4):Trim() )
			Assert.Equal("1,23460", Str(f, 30,5):Trim() )
			Assert.Equal("1,234600", Str(f, 30,6):Trim() )
			Assert.Equal("1,2346000", Str(f, 30,7):Trim() )
			Assert.Equal("1,23460000", Str(f, 30,8):Trim() )
			Assert.Equal("1,234600000", Str(f, 30,9):Trim() )
			Assert.Equal("1,2345999999", Str(f, 30,10):Trim() )
			Assert.Equal("1,23459999990", Str(f, 30,11):Trim() )
			Assert.Equal("1,234599999900", Str(f, 30,12):Trim() )

			Assert.Equal("00010", StrZero(10,5) )
			Assert.Equal("10", StrZero(10,2) )
			Assert.Equal("*", StrZero(10,1) )

			Assert.Equal("-0010", StrZero(-10,5) )
			Assert.Equal("-010", StrZero(-10,4) )
			Assert.Equal("-10", StrZero(-10,3) )
			Assert.Equal("**", StrZero(-10,2) )
			Assert.Equal("*", StrZero(-10,1) )

			Assert.Equal("-00123,123", StrZero(-123.123,10,3) )
			Assert.Equal("-123,123", StrZero(-123.123,8,3) )
			Assert.Equal("-00123,1", StrZero(-123.123,8,1) )
			Assert.Equal("-0123,1", StrZero(-123.123,7,1) )

			Assert.Equal("0000000001", StrZero(1.23 , 10) )
			Assert.Equal("0000000002", StrZero(1.99 , 10) )
			Assert.Equal("00013", StrZero(12.99 , 5) )
			Assert.Equal("13", StrZero(12.99 , 2) )
			Assert.Equal("*", StrZero(12.99 , 1) )
			Assert.Equal("01", StrZero(1.0, 2) )
			Assert.Equal("-0001", StrZero(-1.2345678 , 5) )
			Assert.Equal("-0002", StrZero(-1.99 , 5) )

		[Fact, Trait("Category", "Val")];
		METHOD ValTest() AS VOID
			LOCAL u AS USUAL
            SetDecimalSep('.')
            SetThousandSep(',')
            SetDecimal(2)
			u := Val("1.234")
			Assert.Equal(1.234, (FLOAT) u)
			SetDecimalSep(',')
            SetThousandSep('.')
			u := Val("1,234")
			Assert.Equal(1.234, (FLOAT) u)
            SetDecimalSep('.')
            SetThousandSep(',')
			u := Val("1.23E2")
			Assert.Equal(123.0, (FLOAT) u)
			u := Val("1.2345E2")
			Assert.Equal(123.45, (FLOAT) u)

			Assert.True(Val("0XEE") == 238)
			Assert.True(Val("0xEE") == 238)
			Assert.True(Val("0x100") == 256)
			Assert.True(Val("0x1AE") == 430)

			SetDecimalSep(',')
            SetThousandSep('.')

			u := Val("12,34")
			Assert.Equal(12.34, (FLOAT) u)
			u := Val("12.34")
			Assert.Equal(12.34, (FLOAT) u)

            SetDecimalSep('.')
            SetThousandSep(',')

			u := Val("12,34")
			Assert.Equal(12, (INT) u) // idiotic VO behavior
			u := Val("12.34")
			Assert.Equal(12.34, (FLOAT) u)

            SetDecimalSep('.')
            SetThousandSep(',')

            LOCAL cVal AS STRING
            LOCAL uRet AS USUAL

            cVal := "123456789"; uRet := Val(cVal)
			Assert.Equal(cVal, AsString(uRet))
			Assert.Equal(1, (INT) UsualType(uRet))

            cVal := "1234567890"; uRet := Val(cVal)
			Assert.Equal(cVal, AsString(uRet))
			Assert.Equal(3, (INT) UsualType(uRet))

            cVal := "- 123456789"; uRet := Val(cVal); cVal := "-123456789"
			Assert.Equal(cVal, AsString(uRet))
			Assert.Equal(3, (INT) UsualType(uRet))

            cVal := "   - 123"; uRet := Val(cVal); cVal := "-123"
			Assert.Equal(cVal, AsString(uRet))
			Assert.Equal(1, (INT) UsualType(uRet))

			Assert.Equal("123.456", AsString(Val("123.456")))
			Assert.Equal("123.456", AsString(Val("123.456")))
			Assert.Equal("123.456", AsString(Val("  123.456"  )))
			Assert.Equal("-123.4567", AsString(Val(" - 123.4567")))

			Assert.Equal( "  12345678901234567000", Str(Val("12345678901234567890"),22,-1) )
			Assert.Equal( " -12345678901234567000", Str(Val("-12345678901234567890"),22,-1) )

			Assert.Equal("255", AsString(Val("0xFF")))
			Assert.Equal("-255", AsString(Val("-0xFF")))
			Assert.Equal("-255", AsString(Val(" - 0xFF")))
			Assert.Equal("65534", AsString(Val("0xFFFE")))
			Assert.Equal("2147483648", AsString(Val(" 0X80000000")))

            SetDecimalSep(',')
            SetThousandSep('.')
			Assert.Equal("123,456", AsString(Val("  123.456"  )))
			Assert.Equal("123,456", AsString(Val("  123,456"  )))

			Assert.True(Val(NULL) == 0)

		[Fact, Trait("Category", "Val")];
		METHOD ValTests2() AS VOID
			SetDecimalSep(',')
			SetThousandSep('.')
			Assert.Equal(123, (INT) Val("123") )
			Assert.Equal(123.456, (FLOAT) Val("123,456") )

			Assert.Equal(0, (INT) Val("") )

			Assert.Equal(0, (INT) Val("abc") )
			Assert.Equal(123, (INT) Val("123abc") )
			Assert.Equal(123, (INT) Val("123abc456") )
			Assert.Equal(123, (INT) Val("123abc456,789") )
			Assert.Equal(0, (INT) Val("abc123456,789") )
			Assert.Equal(255, (INT) Val("0xFF") )
			Assert.Equal(0xFFFF, (INT) Val("0xFFFF") )
			Assert.Equal(4294967295, (INT64) Val("0xFFFFFFFF") )
			Assert.Equal(11, (INT) Val("11L11") )

			Assert.Equal(1.000, (FLOAT) Val("1,000.1") )
			Assert.Equal(1.001, (FLOAT) Val("1,001.1") )
			SetDecimalSep('.')
			SetThousandSep(',')
			Assert.Equal(1.0, (FLOAT) Val("1,000.1") )
			Assert.Equal(1.0, (FLOAT) Val("1,001.1") )
		RETURN

//		[Fact, Trait("Category", "Val")];
		#warning Need to add test back when "https://github.com/X-Sharp/XSharpPublic/issues/321" is fixed
		METHOD ValTests_fixed() AS VOID
			LOCAL deci,thou,digit,decimal,fixed_,digitfixed AS USUAL
			deci := SetDecimalSep(Asc("."))
			thou := SetThousandSep(Asc(","))
			digit := SetDigit(12)
			decimal := SetDecimal(3)
			fixed_ := SetFixed(TRUE)
			digitfixed := SetDigitFixed(TRUE)

			Assert.Equal( "*.**", Transform(12.34, "9.99") ) // "1..3", "*.**" in VO
			Assert.Equal( "1234567890", AsString(Val("1234567890")) ) // "1234567890.000", "1234567890" in VO
			Assert.Equal( "-123456789", AsString(Val("- 123456789")) ) // "-123456789.000", "-123456789" in VO
			Assert.Equal( "-123.4567" , AsString(Val(" - 123.4567")) ) // "-123.457", "-123.4567" in VO
			Assert.Equal( "  12345678901234567000", Str(Val("12345678901234567890"),22,-1) )  // "12345678901234567000.0", "  12345678901234567000" in VO
			Assert.Equal( " -12345678901234567000", Str(Val("-12345678901234567890"),22,-1) ) // "-12345678901234567000.", " -12345678901234567000" in VO
			Assert.Equal( "2147483648", AsString(Val(" 0X80000000")) ) // "2147483648.000", "2147483648" in VO

			SetDecimalSep(deci)
			SetThousandSep(thou)
			SetDigit(digit)
			SetDecimal(decimal)
			SetFixed(fixed_)
			SetDigitFixed(digitfixed)
		RETURN
		[Fact, Trait("Category", "DirtyCasts")];
        METHOD DirtyCasts() AS VOID
            Assert.Equal(1, BYTE(_CAST, FTRUE))
            Assert.Equal(0, BYTE(_CAST, FFALSE))
            Assert.Equal(255, _OR(BYTE(_CAST, FTRUE),0xFF))
            Assert.Equal(1, _AND(BYTE(_CAST, FTRUE),0xFF))
            Assert.Equal(1, BYTE(_CAST, TRUE))
            Assert.Equal(0, BYTE(_CAST, FALSE))

		[Fact, Trait("Category", "VariousConversions")];
		METHOD VariousConversions() AS VOID
			LOCAL deci,thou,digit,decimal,fixed_,digitfixed AS USUAL
			deci := SetDecimalSep(Asc(","))
			thou := SetThousandSep(Asc("."))
			digit := SetDigit()
			decimal := SetDecimal(5)
			fixed_ := SetFixed()
			digitfixed := SetDigitFixed()

			LOCAL r AS REAL8
			LOCAL u AS USUAL
			LOCAL f AS FLOAT
			r := 123.456
			u := r
			Assert.Equal( "123,45600", NTrim(u) )
			f := 12.3400000
			Assert.Equal( "12,3400000", NTrim(f) )
			Assert.Equal( "12,3400000", NTrim( AbsFloat(f)) )

			SetDecimalSep(deci)
			SetThousandSep(thou)
			SetDigit(digit)
			SetDecimal(decimal)
			SetFixed(fixed_)
			SetDigitFixed(digitfixed)
		RETURN

		[Fact, Trait("Category", "VariousConversions")];
		METHOD SystemDecimalConversions() AS VOID
			LOCAL deci,thou,decimal AS USUAL
			deci := SetDecimalSep(Asc(","))
			thou := SetThousandSep(Asc("."))
			decimal := SetDecimal(2)

			LOCAL d AS System.Decimal
			LOCAL u AS USUAL
			LOCAL f AS FLOAT
			f := 123.456
			Assert.Equal( 3, f:Decimals )
			Assert.Equal( "123,456", Str(f,-1) )

			u := 123.456
			f := u
			Assert.Equal( 3, f:Decimals )
			Assert.Equal( "123,456", Str(f,-1) )

			d := 123.456m
			f := d
			Assert.Equal( 2, f:Decimals )
			Assert.Equal( "123,46", Str(f,-1) )

			u := d
			f := u
			Assert.Equal( 2, f:Decimals )
			Assert.Equal( "123,46", Str(f,-1) )

			SetDecimal(4)
			f := d
			Assert.Equal( 4, f:Decimals )
			Assert.Equal( "123,4560", Str(f,-1) )

			u := d
			f := u
			Assert.Equal( 4, f:Decimals )
			Assert.Equal( "123,4560", Str(f,-1) )

			SetDecimalSep(deci)
			SetThousandSep(thou)
			SetDecimal(decimal)

			// picture is invalid, but make sure no exception is thrown
			LOCAL val := 1 AS decimal
			LOCAL uVal := val AS USUAL
			? Transform(val, "@R{9999}") // -> no exception
			? Transform(uVal, "@R{9999}") // -> exception
		RETURN

	END CLASS
END NAMESPACE // XSharp.Runtime.Tests

//
// Copyright (c) XSharp B.V.  All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//
USING System
USING System.Collections.Generic
USING System.Linq
USING System.Text
USING XUnit
USING System.Globalization

DEFINE FTRUE  := TRUE
DEFINE FFALSE := FALSE
BEGIN NAMESPACE XSharp.RT.Tests

	CLASS VoConversionTests

		[Fact, Trait("Category", "Conversion")];
		METHOD AsStringTest() AS VOID
			LOCAL u AS USUAL
			u := "123"
			Assert.Equal("123", AsString(u))
			u := #A123
			Assert.Equal("A123", AsString(u))

			Assert.Equal("1", AsString(1))
			VAR c1 := GetRTFullPath()
			Assert.Equal(TRUE, c1:ToLower():IndexOf(".rt.dll") > 0)

			VAR n1 := GetThreadCount()
			Assert.Equal(TRUE, n1 > 1)

			LOCAL p AS PTR
			p := @p
			Assert.Equal(AsString(p), "0x" + AsHexString(p) )
			Assert.Equal(10, AsString(p):Length)
			p := NULL_PTR
			Assert.Equal("0x00000000", AsString(p))
			Assert.Equal("00000000", AsHexString(p))

		[Fact, Trait("Category", "Conversion")];
		METHOD StrTest() AS VOID
			LOCAL c AS STRING
            LOCAL f AS FLOAT
			SetDecimalSep(46)
            SetDecimal(2)
			c := Str3(12.3456,5,2)
			Assert.Equal("12.35", c)	// ROunded up
			c := Str3(12.3411,5,2)
			Assert.Equal("12.34", c)	// ROunded down
            f := FloatFormat(12.3456,10,4)
			c := Str3(f,10,5)
			Assert.Equal("  12.34560", c)	// ROunded down
			c := StrZero(12.3456,10,2)
			Assert.Equal("0000012.35", c)	// ROunded up
			c := Str3(2.49999,4,2)
			Assert.Equal("2.50", c )
			c := Str3(2.50012,4,2)
			Assert.Equal("2.50", c )

			c := Str3(70.00 - 65.01 - 4.99 , 16 , 2)
			Assert.Equal("            0.00", c )
			c := Str3(70.00 - 65.01 - 4.99 , 20 , 15)
			Assert.Equal("  -0.000000000000005", c )

			c := Str3(123456.7890/2.34, 25,15)
			Assert.Equal("    52759.311538461545000", c )
			c := Str(123456.7890/2.34, 25,15)
			Assert.Equal("    52759.311538461545000", c )

			SetDecimalSep(Asc(","))
			f := 1.2345999999
			Assert.Equal("1,2", Str(f, 30,1):Trim() )
			Assert.Equal("1,23", Str(f, 30,2):Trim() )
			Assert.Equal("1,235", Str(f, 30,3):Trim() )
			Assert.Equal("1,2346", Str(f, 30,4):Trim() )
			Assert.Equal("1,23460", Str(f, 30,5):Trim() )
			Assert.Equal("1,234600", Str(f, 30,6):Trim() )
			Assert.Equal("1,2346000", Str(f, 30,7):Trim() )
			Assert.Equal("1,23460000", Str(f, 30,8):Trim() )
			Assert.Equal("1,234600000", Str(f, 30,9):Trim() )
			Assert.Equal("1,2345999999", Str(f, 30,10):Trim() )
			Assert.Equal("1,23459999990", Str(f, 30,11):Trim() )
			Assert.Equal("1,234599999900", Str(f, 30,12):Trim() )

			Assert.Equal("00010", StrZero(10,5) )
			Assert.Equal("10", StrZero(10,2) )
			Assert.Equal("*", StrZero(10,1) )

			Assert.Equal("-0010", StrZero(-10,5) )
			Assert.Equal("-010", StrZero(-10,4) )
			Assert.Equal("-10", StrZero(-10,3) )
			Assert.Equal("**", StrZero(-10,2) )
			Assert.Equal("*", StrZero(-10,1) )

			Assert.Equal("-00123,123", StrZero(-123.123,10,3) )
			Assert.Equal("-123,123", StrZero(-123.123,8,3) )
			Assert.Equal("-00123,1", StrZero(-123.123,8,1) )
			Assert.Equal("-0123,1", StrZero(-123.123,7,1) )

			Assert.Equal("0000000001", StrZero(1.23 , 10) )
			Assert.Equal("0000000002", StrZero(1.99 , 10) )
			Assert.Equal("00013", StrZero(12.99 , 5) )
			Assert.Equal("13", StrZero(12.99 , 2) )
			Assert.Equal("*", StrZero(12.99 , 1) )
			Assert.Equal("01", StrZero(1.0, 2) )
			Assert.Equal("-0001", StrZero(-1.2345678 , 5) )
			Assert.Equal("-0002", StrZero(-1.99 , 5) )

		[Fact, Trait("Category", "Val")];
		METHOD ValTest() AS VOID
			LOCAL u AS USUAL
            SetDecimalSep('.')
            SetThousandSep(',')
            SetDecimal(2)
			u := Val("1.234")
			Assert.Equal(1.234, (FLOAT) u)
			SetDecimalSep(',')
            SetThousandSep('.')
			u := Val("1,234")
			Assert.Equal(1.234, (FLOAT) u)
            SetDecimalSep('.')
            SetThousandSep(',')
			u := Val("1.23E2")
			Assert.Equal(123.0, (FLOAT) u)
			u := Val("1.2345E2")
			Assert.Equal(123.45, (FLOAT) u)

			Assert.True(Val("0XEE") == 238)
			Assert.True(Val("0xEE") == 238)
			Assert.True(Val("0x100") == 256)
			Assert.True(Val("0x1AE") == 430)

			SetDecimalSep(',')
            SetThousandSep('.')

			u := Val("12,34")
			Assert.Equal(12.34, (FLOAT) u)
			u := Val("12.34")
			Assert.Equal(12.34, (FLOAT) u)

            SetDecimalSep('.')
            SetThousandSep(',')

			u := Val("12,34")
			Assert.Equal(12, (INT) u) // idiotic VO behavior
			u := Val("12.34")
			Assert.Equal(12.34, (FLOAT) u)

            SetDecimalSep('.')
            SetThousandSep(',')

            LOCAL cVal AS STRING
            LOCAL uRet AS USUAL

            cVal := "123456789"; uRet := Val(cVal)
			Assert.Equal(cVal, AsString(uRet))
			Assert.Equal(1, (INT) UsualType(uRet))

            cVal := "1234567890"; uRet := Val(cVal)
			Assert.Equal(cVal, AsString(uRet))
			Assert.Equal(3, (INT) UsualType(uRet))

            cVal := "- 123456789"; uRet := Val(cVal); cVal := "-123456789"
			Assert.Equal(cVal, AsString(uRet))
			Assert.Equal(3, (INT) UsualType(uRet))

            cVal := "   - 123"; uRet := Val(cVal); cVal := "-123"
			Assert.Equal(cVal, AsString(uRet))
			Assert.Equal(1, (INT) UsualType(uRet))

			Assert.Equal("123.456", AsString(Val("123.456")))
			Assert.Equal("123.456", AsString(Val("123.456")))
			Assert.Equal("123.456", AsString(Val("  123.456"  )))
			Assert.Equal("-123.4567", AsString(Val(" - 123.4567")))

			Assert.Equal( "  12345678901234567000", Str(Val("12345678901234567890"),22,-1) )
			Assert.Equal( " -12345678901234567000", Str(Val("-12345678901234567890"),22,-1) )

			Assert.Equal("255", AsString(Val("0xFF")))
			Assert.Equal("-255", AsString(Val("-0xFF")))
			Assert.Equal("-255", AsString(Val(" - 0xFF")))
			Assert.Equal("65534", AsString(Val("0xFFFE")))
			Assert.Equal("2147483648", AsString(Val(" 0X80000000")))

            SetDecimalSep(',')
            SetThousandSep('.')
			Assert.Equal("123,456", AsString(Val("  123.456"  )))
			Assert.Equal("123,456", AsString(Val("  123,456"  )))

			Assert.True(Val(NULL) == 0)

		[Fact, Trait("Category", "Val")];
		METHOD ValTests2() AS VOID
			SetDecimalSep(',')
			SetThousandSep('.')
			Assert.Equal(123, (INT) Val("123") )
			Assert.Equal(123.456, (FLOAT) Val("123,456") )

			Assert.Equal(0, (INT) Val("") )

			Assert.Equal(0, (INT) Val("abc") )
			Assert.Equal(123, (INT) Val("123abc") )
			Assert.Equal(123, (INT) Val("123abc456") )
			Assert.Equal(123, (INT) Val("123abc456,789") )
			Assert.Equal(0, (INT) Val("abc123456,789") )
			Assert.Equal(255, (INT) Val("0xFF") )
			Assert.Equal(0xFFFF, (INT) Val("0xFFFF") )
			Assert.Equal(4294967295, (INT64) Val("0xFFFFFFFF") )
			Assert.Equal(11, (INT) Val("11L11") )

			Assert.Equal(1.000, (FLOAT) Val("1,000.1") )
			Assert.Equal(1.001, (FLOAT) Val("1,001.1") )
			SetDecimalSep('.')
			SetThousandSep(',')
			Assert.Equal(1.0, (FLOAT) Val("1,000.1") )
			Assert.Equal(1.0, (FLOAT) Val("1,001.1") )
		RETURN

//		[Fact, Trait("Category", "Val")];
		#warning Need to add test back when "https://github.com/X-Sharp/XSharpPublic/issues/321" is fixed
		METHOD ValTests_fixed() AS VOID
			LOCAL deci,thou,digit,decimal,fixed_,digitfixed AS USUAL
			deci := SetDecimalSep(Asc("."))
			thou := SetThousandSep(Asc(","))
			digit := SetDigit(12)
			decimal := SetDecimal(3)
			fixed_ := SetFixed(TRUE)
			digitfixed := SetDigitFixed(TRUE)

			Assert.Equal( "*.**", Transform(12.34, "9.99") ) // "1..3", "*.**" in VO
			Assert.Equal( "1234567890", AsString(Val("1234567890")) ) // "1234567890.000", "1234567890" in VO
			Assert.Equal( "-123456789", AsString(Val("- 123456789")) ) // "-123456789.000", "-123456789" in VO
			Assert.Equal( "-123.4567" , AsString(Val(" - 123.4567")) ) // "-123.457", "-123.4567" in VO
			Assert.Equal( "  12345678901234567000", Str(Val("12345678901234567890"),22,-1) )  // "12345678901234567000.0", "  12345678901234567000" in VO
			Assert.Equal( " -12345678901234567000", Str(Val("-12345678901234567890"),22,-1) ) // "-12345678901234567000.", " -12345678901234567000" in VO
			Assert.Equal( "2147483648", AsString(Val(" 0X80000000")) ) // "2147483648.000", "2147483648" in VO

			SetDecimalSep(deci)
			SetThousandSep(thou)
			SetDigit(digit)
			SetDecimal(decimal)
			SetFixed(fixed_)
			SetDigitFixed(digitfixed)
		RETURN
		[Fact, Trait("Category", "DirtyCasts")];
        METHOD DirtyCasts() AS VOID
            Assert.Equal(1, BYTE(_CAST, FTRUE))
            Assert.Equal(0, BYTE(_CAST, FFALSE))
            Assert.Equal(255, _OR(BYTE(_CAST, FTRUE),0xFF))
            Assert.Equal(1, _AND(BYTE(_CAST, FTRUE),0xFF))
            Assert.Equal(1, BYTE(_CAST, TRUE))
            Assert.Equal(0, BYTE(_CAST, FALSE))

		[Fact, Trait("Category", "VariousConversions")];
		METHOD VariousConversions() AS VOID
			LOCAL deci,thou,digit,decimal,fixed_,digitfixed AS USUAL
			deci := SetDecimalSep(Asc(","))
			thou := SetThousandSep(Asc("."))
			digit := SetDigit()
			decimal := SetDecimal(5)
			fixed_ := SetFixed()
			digitfixed := SetDigitFixed()

			LOCAL r AS REAL8
			LOCAL u AS USUAL
			LOCAL f AS FLOAT
			r := 123.456
			u := r
			Assert.Equal( "123,45600", NTrim(u) )
			f := 12.3400000
			Assert.Equal( "12,3400000", NTrim(f) )
			Assert.Equal( "12,3400000", NTrim( AbsFloat(f)) )

			SetDecimalSep(deci)
			SetThousandSep(thou)
			SetDigit(digit)
			SetDecimal(decimal)
			SetFixed(fixed_)
			SetDigitFixed(digitfixed)
		RETURN

	END CLASS
END NAMESPACE // XSharp.Runtime.Tests


[VFP] Store values of runtime properties outside of the descriptor because these are shared between instances of an object

//
// Copyright (c) XSharp B.V.  All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//

using System
using System.Collections.Generic
using System.Diagnostics
using System.Linq
using System.Reflection

class XSharp.VFP.PropertyContainer

    static _PropertyCache as Dictionary<System.Type, IList<PropertyDescriptor> >

    static constructor
        _PropertyCache :=  Dictionary<System.Type, IList<PropertyDescriptor> > {}

    protected _Properties as Dictionary<string, PropertyDescriptor>
    protected _Values     as Dictionary<string, usual>
    protected _Owner      as object

    property Count as long get _Properties:Count
    constructor(oOwner as object)
        _Properties := Dictionary<string, PropertyDescriptor>{StringComparer.OrdinalIgnoreCase}
        _Values     := Dictionary<string, usual>{StringComparer.OrdinalIgnoreCase}
        _Owner := oOwner
        self:InitCompileTimeProperties()

    private method InitCompileTimeProperties() as void
        var oType := _Owner:GetType()
        local list as IList<PropertyDescriptor>
        if _PropertyCache:ContainsKey(oType)
            list := _PropertyCache[oType]
            foreach var item in list
                _Properties[item:Name] := item
            next
            return
        endif
        var aProps := _Owner:GetType():GetProperties()
        list := List<PropertyDescriptor>{}
        foreach var oProp in aProps
            var met := oProp:GetGetMethod(true)
            if met != null
                var nVis := iif(met:IsPublic,1 ,iif(met:IsPrivate,3,2))
                var desc := self:Add(oProp:Name, nil, nVis,nil)
                if _Values:ContainsKey(oProp:Name)
                    _Values:Remove(oProp:Name)
                endif
                list:Add(desc)
                _Properties[oProp:Name]:PropInfo := oProp
            endif
        next
        _PropertyCache[oType] := list
        return

       #region Property related
    internal method Add(cName as string, uValue as usual, nVisibility as usual, cDescription as usual) as PropertyDescriptor
        // Note that we need to handle the syntax AddProperty("PropertyName(3)") which adds an array property with 3 elements
        if ! String.IsNullOrEmpty(cName)
            local cDims := String.Empty as string
            cName := cName:Trim()
            if cName:EndsWith(")")
                var nPos := cName:IndexOf("(")
                if nPos > 0 // we need at least 1 character
                    cDims := cName:Substring(nPos)
                    cDims := cDims:Substring(1, cDims:Length-2)
                    cName := cName:Substring(0, nPos)
                endif
            endif
            if ! String.IsNullOrEmpty(cDims)
                var aDims := cDims:Split(c',')
                local dims as usual[]
                dims := usual[] {aDims:Length}
                for var i := 1 to aDims:Length
                    dims[i] := Int32.Parse(aDims[i])
                next
                local aValue as __FoxArray
                aValue := __FoxArray{0}
                if (dims:Length == 1)
                    aValue:ReDim(dims[1])
                else
                    aValue:ReDim(dims[1], dims[2])
                endif
                uValue := aValue
            endif
            local nPropVis  := PropertyVisibility.Public as PropertyVisibility
            local cPropDesc := "" as string
            if IsNumeric(nVisibility) .or. IsString(cDescription)
                if IsNumeric(nVisibility)
                    nPropVis := (PropertyVisibility) nVisibility
                endif
                if IsString(cDescription)
                    cPropDesc := cDescription
                endif
             endif
             var desc := PropertyDescriptor{cName, nPropVis, cPropDesc}
             _Properties[cName]:= desc
             _Values[cName] := uValue
             return desc
        endif
        return null

    internal method Remove(cPropertyName as string) as logic
        // FoxPro does not throw an error when non existing properties are removed
        // FoxPro does not require the dimensions when deleting an array property
        if _Values:ContainsKey(cPropertyName)
            _Values:Remove(cPropertyName)
        endif
        if _Properties:ContainsKey(cPropertyName)
            local desc := _Properties[cPropertyName] as PropertyDescriptor
            // you cannot remove builtin properties, only dynamic properties
            if desc:PropInfo == null
                _Properties:Remove(cPropertyName)
            endif
            return true
        endif
        return false

        #endregion

    #region IDynamicProperties
    virtual method NoIvarPut(cName as string, uValue as usual) as void
        if _Properties:ContainsKey( cName)
            var desc := _Properties[cName]
            if desc:PropInfo != null
                desc:PropInfo:SetValue(_Owner, uValue)
                return
            elseif self:_Values:ContainsKey(cName)
                self:_Values[cName] := uValue
                return
            endif
        endif
        throw PropertyNotFoundException{cName}

    virtual method NoIvarGet(cName as string) as usual
        if _Properties:ContainsKey(cName)
            var desc := _Properties[cName]
            if desc:PropInfo != null
                return desc:PropInfo:GetValue(_Owner)
            elseif self:_Values:ContainsKey(cName)
                return self:_Values[cName]
            endif
        endif
        throw PropertyNotFoundException{cName}

    virtual method GetPropertyNames() as string[]
        return _Properties:Keys:ToArray()

    #endregion

     internal method GetProperties() as List<NameValuePair>
        var result := List<NameValuePair>{}
        foreach var item in _Properties
            local desc as PropertyDescriptor
            var uValue := self:NoIvarGet(item:Key)
            desc := item:Value
            if desc:PropInfo == null .or. desc:PropInfo:PropertyType != typeof(PropertyContainer)

                result:Add( NameValuePair{}{Name := item:Key, @@Value := uValue})
            endif
        next
        return result

end class

public class XSharp.VFP.PropertyDescriptor
    public Name           as string
    public Visibility     as PropertyVisibility
    public Description    as string
    public PropInfo       as PropertyInfo
    public constructor(cName as string, nVis as PropertyVisibility, cDesc as string)
        Name        := cName
        Visibility  := nVis
        Description := cDesc

end class

public class XSharp.VFP.PropertyNotFoundException inherit Exception
    public property PropertyName as string auto get private set
    public constructor(name as string)
        super(__VfpStr(VFPErrors.PROPERTY_NOT_FOUND, name))
        self:PropertyName := name
        return
end class

//
// Copyright (c) XSharp B.V.  All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//

using System
using System.Collections.Generic
using System.Diagnostics
using System.Linq
using System.Reflection

class XSharp.VFP.PropertyContainer implements XSharp.IDynamicProperties

    static _PropertyCache as Dictionary<System.Type, IList<PropertyDescriptor> >

    static constructor
        _PropertyCache :=  Dictionary<System.Type, IList<PropertyDescriptor> > {}

    protected _Properties as Dictionary<string, PropertyDescriptor>
    protected _Owner as object

    property Count as long get _Properties:Count
    constructor(oOwner as object)
        _Properties := Dictionary<string, PropertyDescriptor>{StringComparer.OrdinalIgnoreCase}
        _Owner := oOwner
        self:InitCompileTimeProperties()

    private method InitCompileTimeProperties() as void
        var oType := _Owner:GetType()
        local list as IList<PropertyDescriptor>
        if _PropertyCache:ContainsKey(oType)
            list := _PropertyCache[oType]
            foreach var item in list
                _Properties[item:Name] := item
            next
            return
        endif
        var aProps := _Owner:GetType():GetProperties()
        list := List<PropertyDescriptor>{}
        foreach var oProp in aProps
            var met := oProp:GetGetMethod(true)
            if met != null
                var nVis := iif(met:IsPublic,1 ,iif(met:IsPrivate,3,2))
                var desc := self:Add(oProp:Name, nil, nVis,nil)
                list:Add(desc)
                _Properties[oProp:Name]:PropInfo := oProp
            endif
        next
        _PropertyCache[oType] := list
        return

       #region Property related
    internal method Add(cName as string, uValue as usual, nVisibility as usual, cDescription as usual) as PropertyDescriptor
        // Note that we need to handle the syntax AddProperty("PropertyName(3)") which adds an array property with 3 elements
        if ! String.IsNullOrEmpty(cName)
            local cDims := String.Empty as string
            cName := cName:Trim()
            if cName:EndsWith(")")
                var nPos := cName:IndexOf("(")
                if nPos > 0 // we need at least 1 character
                    cDims := cName:Substring(nPos)
                    cDims := cDims:Substring(1, cDims:Length-2)
                    cName := cName:Substring(0, nPos)
                endif
            endif
            if ! String.IsNullOrEmpty(cDims)
                var aDims := cDims:Split(c',')
                local dims as usual[]
                dims := usual[] {aDims:Length}
                for var i := 1 to aDims:Length
                    dims[i] := Int32.Parse(aDims[i])
                next
                local aValue as __FoxArray
                aValue := __FoxArray{0}
                if (dims:Length == 1)
                    aValue:ReDim(dims[1])
                else
                    aValue:ReDim(dims[1], dims[2])
                endif
                uValue := aValue
            endif
            local nPropVis  := PropertyVisibility.Public as PropertyVisibility
            local cPropDesc := "" as string
            if IsNumeric(nVisibility) .or. IsString(cDescription)
                if IsNumeric(nVisibility)
                    nPropVis := (PropertyVisibility) nVisibility
                endif
                if IsString(cDescription)
                    cPropDesc := cDescription
                endif
             endif
             var desc := PropertyDescriptor{cName, nPropVis, cPropDesc}
             desc:Value := uValue
             _Properties[cName]:= desc
             return desc
        endif
        return null

    internal method Remove(cPropertyName) as logic clipper
        // FoxPro does not throw an error when non existing properties are removed
        // FoxPro does not require the dimensions when deleting an array property
        if _Properties:ContainsKey(cPropertyName)
            _Properties:Remove(cPropertyName)
            return true
        endif
        return false

        #endregion

    #region IDynamicProperties
    virtual method NoIvarPut(cName as string, uValue as usual) as void
        if _Properties:ContainsKey( cName)
            var desc := _Properties[cName]
            desc:SetValue(_Owner, uValue)
        else
            throw PropertyNotFoundException{cName}
        endif
        return

    virtual method NoIvarGet(cName as string) as usual
        if _Properties:ContainsKey(cName)
            var desc := _Properties[cName]
            return desc:GetValue(_Owner)
        else
            throw PropertyNotFoundException{cName}
        endif

    virtual method GetPropertyNames() as string[]
        return _Properties:Keys:ToArray()

    #endregion

    #region Implementations
    public method Get(cName as string) as usual
        if _Properties:ContainsKey(cName)
            return _Properties[cName]:Value
        else
            throw PropertyNotFoundException{cName}
        endif

    public method Set(cName as string, uValue as usual) as void
        if _Properties:ContainsKey(cName)
            _Properties[cName]:Value := uValue
        else
            throw PropertyNotFoundException{cName}
        endif

    #endregion

     internal method GetProperties() as List<NameValuePair>
        var result := List<NameValuePair>{}
        foreach var item in _Properties
            local desc as PropertyDescriptor
            desc := item:Value
            if desc:PropInfo == null .or. desc:PropInfo:PropertyType != typeof(PropertyContainer)
                result:Add( NameValuePair{}{Name := item:Key, @@Value := desc:GetValue(_Owner)})
            endif
        next
        return result

end class

public class XSharp.VFP.PropertyDescriptor
    public Name           as string
    public Visibility     as PropertyVisibility
    public Description    as string
    public @@Value        as object
    public PropInfo       as PropertyInfo
    public constructor(cName as string, nVis as PropertyVisibility, cDesc as string)
        Name        := cName
        Visibility  := nVis
        Description := cDesc
        @@Value     := nil
    public method GetValue(oObject as object) as object
        if PropInfo != null
            return PropInfo:GetValue(oObject)
        endif
        return self:@@Value
    public method SetValue(oObject as object, oValue as object) as void
        if PropInfo != null
            PropInfo:SetValue(oObject, oValue)
        else
            self:@@Value  := oValue
        endif

end class

public class XSharp.VFP.PropertyNotFoundException inherit Exception
    public property PropertyName as string auto get private set
    public constructor(name as string)
        super(__VfpStr(VFPErrors.PROPERTY_NOT_FOUND, name))
        self:PropertyName := name
        return
end class