[Runtime] Comment updated

[Runtime] Comment updated

//
// 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.
//
// Most of these settings will Get and Set properties of the Runtime.State class
#include "GetSet.xh"

USING XSharp

///
/// Returns a string representing the morning extension for time strings in 12-hour format.
///
/// <returns>
/// </returns>
FUNCTION GetAMExt() AS STRING
	GETSTATE STRING Set.AmExt

///
/// Returns a string representing the morning extension for time strings in 12-hour format.
///
/// <returns>
/// </returns>
FUNCTION SetAMExt() AS STRING
	GETSTATE STRING Set.AmExt

///
/// Set the morning extension for time strings in 12-hour format.
///
/// <param name="cExt"></param>
/// <returns>
/// </returns>
FUNCTION SetAMExt(cExt AS STRING) AS STRING
	SETSTATE STRING Set.AmExt cExt

///
/// Returns the setting that determines whether time strings are in 12-hour or 24-hour format.
///
/// <returns>
/// </returns>
FUNCTION GetAmPm() AS LOGIC
	GETSTATE LOGIC Set.AmPm

///
/// Returns the setting that determines whether time strings are in 12-hour or 24-hour format.
///
/// <returns>
/// </returns>
FUNCTION SetAmPm() AS LOGIC
	GETSTATE LOGIC Set.AmPm

///
/// Change the setting that determines whether time strings are in 12-hour or 24-hour format.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetAmPm(lSet AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.AmPm lSet

///
/// Return and the setting that determines whether database files are created using ANSI or OEM format and whether certain text file operations convert between the two character sets.
///
/// <returns>
/// </returns>
FUNCTION SetAnsi() AS LOGIC
	RETURN RuntimeState.Ansi

///
/// Change the setting that determines whether database files are created using ANSI or OEM format and whether certain text file operations convert between the two character sets.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetAnsi(lSet AS LOGIC) AS LOGIC
	LOCAL lOld := RuntimeState.Ansi AS LOGIC
	RuntimeState.Ansi := lSet
	RETURN lOld

///
/// Return the setting that determines whether a beep is sounded by the error system when an error occurs.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetBeep() AS LOGIC
	GETSTATE LOGIC Set.BELL

///
/// Change the setting that determines whether a beep is sounded by the error system when an error occurs.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetBeep(lSet AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.BELL lSet

///
/// Return the setting that determines whether to include or omit century digits in the date format.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetCentury() AS LOGIC
	GETSTATE LOGIC Set.Century

///
/// Change the setting that determines whether to include or omit century digits in the date format.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetCentury(lSet AS LOGIC) AS LOGIC
	LOCAL lOld AS LOGIC
    lOld := XSharp.RuntimeState.GetValue<LOGIC>(Set.CENTURY)
    IF lOld != lSet
      VAR cFormat := XSharp.RuntimeState.DateFormat

      IF lSet
        IF ! cFormat:Contains("YYYY")
            cFormat := cFormat:Replace( "YY", "YYYY" )
        ENDIF
        ELSE
            IF cFormat:Contains("YYYY")
                cFormat := cFormat:Replace( "YYYY", "YY" )
            ENDIF
      ENDIF

      SetDateFormat( cFormat )

    ENDIF
    RETURN lOld

///
/// Return the setting that determines the type of central processor you have.
///
/// <param name="nCpu"></param>
/// <returns>
/// </returns>
FUNCTION SetCpu() AS DWORD
	GETSTATE DWORD Set.CPU

///
/// Change the setting that determines the type of central processor you have.
///
/// <param name="nCpu"></param>
/// <returns>
/// </returns>
FUNCTION SetCpu(nCpu AS DWORD) AS DWORD
	SETSTATE DWORD Set.CPU nCPU

///
/// Return the setting that determines the X# date format by selecting from a list of constants with corresponding date formats.
///
/// <param name="dwCountry"></param>
/// <returns>
/// </returns>
FUNCTION SetDateCountry() AS DWORD
	GETSTATE DWORD Set.DATECOUNTRY

///
/// Return and optionally change the setting that determines the X# date format by selecting from a list of constants with corresponding date formats.
///
/// <param name="dwCountry"></param>
/// <returns>
/// </returns>
FUNCTION SetDateCountry(dwCountry AS DWORD) AS DWORD
//	setstate DWORD Set.DATECOUNTRY dwCountry
	LOCAL dwOld := RuntimeState.DateCountry AS DWORD
	RuntimeState.DateCountry := dwCountry
	RETURN dwOld

///
/// Return the current Date format.
///
/// <returns>
/// </returns>
FUNCTION GetDateFormat() AS STRING
	GETSTATE STRING Set.DateFormat

///
/// Change the setting that determines the X# date format.
///
/// <param name="cDateFormat"></param>
/// <returns>
/// </returns>
FUNCTION SetDateFormat(cDateFormat AS STRING) AS STRING
	LOCAL cOld AS STRING
	// Changing Dateformat also changes DateCountry and Century
	cOld := RuntimeState.DateFormat
	RuntimeState.DateFormat := cDateFormat
	RETURN cOld

///
/// Return the setting that determines the number of decimal places used to display numbers.
///
/// <returns>
/// </returns>
FUNCTION SetDecimal() AS DWORD
	GETSTATE DWORD Set.Decimals

///
/// Return and change the setting that determines the number of decimal places used to display numbers.
///
/// <param name="nDec"></param>
/// <returns>
/// </returns>
FUNCTION SetDecimal(nDec AS DWORD) AS DWORD
	SETSTATE DWORD Set.Decimals nDec

///
/// Return the setting that determines the decimal separation character to be used in numeric-to-string conversion functions.
///
/// <returns>
/// </returns>

FUNCTION SetDecimalSep() AS DWORD
	GETSTATE DWORD Set.DecimalSep

///
/// Return and change the setting that determines the decimal separation character to be used in numeric-to-string conversion functions.
///
/// <param name="wSep"></param>
/// <returns>
/// </returns>
FUNCTION SetDecimalSep(wSep AS DWORD) AS DWORD
	LOCAL oCulture AS System.Globalization.CultureInfo
	oCulture := (System.Globalization.CultureInfo)System.Threading.Thread.CurrentThread:CurrentCulture:Clone()
	oCulture:NumberFormat:NumberDecimalSeparator := ((CHAR)wSep):ToString()
	System.Threading.Thread.CurrentThread:CurrentCulture := oCulture

	SETSTATE DWORD Set.DecimalSep wSep

///
/// Return the setting that determines the default drive and directory.
///
/// <param name="cDefault"></param>
/// <returns>
/// </returns>
FUNCTION SetDefault() AS STRING
	GETSTATE STRING Set.Default

///
/// Change the setting that determines the default drive and directory.
///
/// <param name="cDefault"></param>
/// <returns>
/// </returns>
FUNCTION SetDefault(cDefault AS STRING) AS STRING
	SetPathArray(NULL)
	SETSTATE STRING Set.Default cDefault

///
/// Return the setting that determines whether to ignore or include records that are marked for deletion.
///
/// <returns>
/// </returns>
FUNCTION SetDeleted() AS LOGIC
	GETSTATE LOGIC Set.Deleted

///
/// Change the setting that determines whether to ignore or include records that are marked for deletion.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetDeleted(lSet AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.Deleted lSet

///
/// Return the setting that determines the number of digits that will be shown to the left of the decimal point when a number is displayed.
///
/// <returns>
/// </returns>
FUNCTION SetDigit() AS DWORD
	GETSTATE DWORD Set.DIGITS

///
/// Change the setting that determines the number of digits that will be shown to the left of the decimal point when a number is displayed.
///
/// <param name="nDig"></param>
/// <returns>
/// </returns>
FUNCTION SetDigit(nDig AS DWORD) AS DWORD
	SETSTATE DWORD Set.DIGITS nDIg

///
/// Return the setting that fixes the number of digits used to display numeric output.
///
/// <param name="f"></param>
/// <returns>
/// </returns>
FUNCTION SetDigitFixed() AS LOGIC
	GETSTATE LOGIC Set.DigitFixed

///
/// Change the setting that fixes the number of digits used to display numeric output.
///
/// <param name="f"></param>
/// <returns>
/// </returns>
FUNCTION SetDigitFixed(f AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.DigitFixed f

///
/// Return the setting that determines how dates without century digits are interpreted.
///
/// <returns>
/// </returns>
FUNCTION SetEpoch() AS DWORD
	GETSTATE DWORD Set.Epoch

///
/// Change the setting that determines how dates without century digits are interpreted.
///
/// <param name="wYear"></param>
/// <returns>
/// </returns>
FUNCTION SetEpoch(wEpoch AS DWORD) AS DWORD
	LOCAL wYear AS DWORD
	LOCAL wCent AS DWORD
	wYear := wEpoch % 100
	wCent := (( wEpoch / 100) +1) * 100
	XSharp.RuntimeState.SetValue<DWORD> (Set.EpochYear, wYear)
	XSharp.RuntimeState.SetValue<DWORD> (Set.EpochCent, wCent)
	SETSTATE DWORD Set.Epoch	 wEpoch

///
/// Return the setting that determines whether error information is written to the error log file by the default runtime error handler.
///
/// <returns>
/// </returns>
FUNCTION SetErrorLog() AS LOGIC
	GETSTATE LOGIC Set.ERRRORLOG

///
/// Change the setting that determines whether error information is written to the error log file by the default runtime error handler.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetErrorLog(lSet AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.ERRRORLOG lSet

///
/// Return the setting for an exact match for character string comparisons.
///
/// <returns>
/// </returns>
FUNCTION SetExact() AS LOGIC
	GETSTATE LOGIC Set.Exact

///
/// Change the setting for an exact match for character string comparisons.
///
/// <param name="fExact"></param>
/// <returns>
/// </returns>
FUNCTION SetExact(fExact AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.Exact fExact

///
/// Return the setting that determines whether to open database files in exclusive or shared mode.
///
/// <returns>
/// </returns>
FUNCTION SetExclusive() AS LOGIC
	GETSTATE LOGIC Set.Exclusive

///
/// Change the setting that determines whether to open database files in exclusive or shared mode.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetExclusive(lSet AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.Exclusive lSet

///
/// Return the setting that determines whether assignments are made to fields or to memory variables.
///
/// <returns>
/// </returns>
FUNCTION SetFieldStore() AS LOGIC
	GETSTATE LOGIC Set.FieldStore

///
/// Change the setting that determines whether assignments are made to fields or to memory variables.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetFieldStore(lSet AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.FieldStore lSet

///
/// Return the setting that fixes the number of decimal digits used to display numbers.
///
/// <returns>
/// </returns>
FUNCTION SetFixed() AS LOGIC
	GETSTATE LOGIC Set.Fixed

///
/// Change the setting that fixes the number of decimal digits used to display numbers.
///
/// <param name="fFixed"></param>
/// <returns>
/// </returns>
FUNCTION SetFixed(fFixed AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.Fixed fFixed

///
/// Return the setting that determines the internal operational characteristics of the underlying floating-point system.
///
/// <returns>
/// </returns>
FUNCTION SetMath() AS DWORD
	GETSTATE DWORD Set.MATH

///
/// Change the setting that determines the internal operational characteristics of the underlying floating-point system.
///
/// <param name="nFPU"></param>
/// <returns>
/// </returns>
FUNCTION SetMath(nFPU AS DWORD) AS DWORD
	SETSTATE DWORD Set.MATH nFPU

///
/// Activate a new DLL for nation-dependent operations and messages.
///
/// <returns>
/// </returns>
/// <remarks>
/// In Visual Objects the nation DLL is an external DLL. For X# there are no separate
/// nation DLL. The nation support code is integrated in XSharp.Core.DLL.
/// For compatibility you can still use the SetNatDLL() function to switch to another
/// nation module.

/// The available nation modules are (just like in Visual Objects):

/// 	"BRAZIL", "CROATIA", "CZECH852", "CZECH895", "DANISH", "DUTCH", "FINNISH",
/// 	"FRENCH", "GENERIC", "GERMAN", "GERMAN2", "HUNG852", "HUNGCWI", "ITALIAN",
///     "NORWEGN", "POL-ISO", "POL-MAZ", "POL852", "PORT850", "PORT860", "ROMANIA",
/// 	"RUSSIAN", "SERBIA", "SL-W-95", "SL-W-AS7", "SL-W-EE", "SLOV852", "SLOV895",
/// 	"SPANISH", "SWEDISH", "UK"
///
 You may specify a DLL name with full path. The Runtime will strip the path and extension
/// from the file name and will look for one of the names listed above.

/// The nation DLL name is NOT case sensitive.
///</remarks>

FUNCTION SetNatDLL(cNewDLL AS STRING) AS LOGIC
	LOCAL cBase AS STRING
	_SetNatDLL(cnewDLL)
	cBase := System.IO.Path.GetFileNameWithoutExtension(cNewDLL)
	_SetCollation(cBase)
	RETURN String.Compare(Messages.CurrentLanguageName, cBase, TRUE) == 0

/// <exclude/>
FUNCTION _SetCollation(cBase AS STRING) AS LOGIC
	VAR rm := System.Resources.ResourceManager{ "XSharp.Collations", TYPEOF(Functions):Assembly }
	VAR obj := rm:GetObject(cBase:ToUpper())
	IF obj != NULL
		VAR bytes := obj ASTYPE BYTE[]
		IF bytes != NULL
			XSharp.RuntimeState.CollationTable := bytes
			RETURN TRUE
		ENDIF
	ENDIF
	RETURN FALSE
/// <exclude/>
INTERNAL FUNCTION	_SetNatDLL(cNewDLL AS STRING) AS STRING
	LOCAL cBase AS STRING
	cBase := System.IO.Path.GetFileNameWithoutExtension(cNewDLL)
	Messages.SetCurrentLanguage(cBase)
	SETSTATE STRING Set.NatDLL cNewDLL

///
/// Return the setting that determines the search path for opening files. This may be a semi colon separated list of folders.
///
/// <returns>
/// </returns>
FUNCTION SetPath() AS STRING
	GETSTATE STRING Set.Path

/// <exclude/>
FUNCTION _SetDict() AS LOGIC
    GETSTATE LOGIC Set.DICT

/// <exclude/>
FUNCTION _SetDict(lNewSetting AS LOGIC) AS LOGIC
    SETSTATE LOGIC Set.DICT lNewSetting

///
/// Change the setting that determines the search path for opening files.
///
/// <param name="cPath">New path. This may be a semi colon separated list of folders.</param>
/// <returns>
/// </returns>
FUNCTION SetPath(cPath AS STRING) AS STRING
	SetPathArray(NULL)
	SETSTATE STRING Set.Path cPath

///
/// Return the Path array that is used by the File() function to locate files outside of the current directory.
/// This is a combination of the SetDefault() and SetPath() variables
/// This may be null if the file function has never been called or never been called for files outside of the current
/// directory.
///
/// <returns>
/// </returns>
FUNCTION SetPathArray() AS STRING[]
	GETSTATE STRING[] Set.PathArray

///
/// Set the Path array that is used by the File() function to locate files outside of the current directory.
/// This is a combination of the SetDefault() and SetPath() variables
/// This array gets cleared when SetPath() or SetDefault() is called and is initialized the first time File() or a related function
//  is called after the path has been changed.
///
/// <param name="aPath"></param>
/// <returns>
/// </returns>
FUNCTION SetPathArray(aPath AS STRING[]) AS STRING[]
	SETSTATE STRING[] Set.PathArray aPath

///
/// Returns a string representing the evening extension for time strings in 12-hour format.
///
/// <returns>
/// </returns>
FUNCTION SetPMExt() AS STRING
	GETSTATE STRING Set.PmExt

///
/// Set the evening extension for time strings in 12-hour format.
///
/// <param name="cExt"></param>
/// <returns>
/// </returns>
FUNCTION SetPMExt(cExt AS STRING) AS STRING
	SETSTATE STRING Set.PmExt cExt

///
/// Return the setting that displays numbers in scientific notation.
///
/// <returns>
/// </returns>
FUNCTION SetScience() AS LOGIC
	GETSTATE LOGIC Set.Science

///
/// Change the setting that displays numbers in scientific notation.
///
/// <param name="f"></param>
/// <returns>
/// </returns>
FUNCTION SetScience(lSet AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.Science lSet

///
/// Return the setting that determines whether a seek operation will find a close match when no exact match is found.
///
/// <returns>
/// </returns>
FUNCTION SetSoftSeek() AS LOGIC
	GETSTATE LOGIC Set.SoftSeek

///
/// Change the setting that determines whether a seek operation will find a close match when no exact match is found.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetSoftSeek(lSet AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.SoftSeek lSet

///
/// Return the setting that determines the thousands separation character to be used in numeric-to-string conversion functions.
///
/// <returns>
/// </returns>
FUNCTION SetThousandSep() AS DWORD
	GETSTATE DWORD Set.ThousandSep

///
/// Change the setting that determines the thousands separation character to be used in numeric-to-string conversion functions.
///
/// <param name="wSep"></param>
/// <returns>
/// </returns>
FUNCTION SetThousandSep(wSep AS DWORD) AS DWORD
	LOCAL oCulture AS System.Globalization.CultureInfo
	oCulture := (System.Globalization.CultureInfo)System.Threading.Thread.CurrentThread:CurrentCulture:Clone()
	oCulture:NumberFormat:NumberGroupSeparator := ((CHAR)wSep):ToString()
	System.Threading.Thread.CurrentThread:CurrentCulture := oCulture

	SETSTATE DWORD Set.ThousandSep wSep

///
/// Return the setting that determines the separation character to be used in time strings.
///
/// <returns>
/// </returns>
FUNCTION SetTimeSep() AS DWORD
	GETSTATE DWORD Set.TimeSep

///
/// Change the setting that determines the separation character to be used in time strings.
///
/// <param name="dwChar"></param>
/// <returns>
/// </returns>
FUNCTION SetTimeSep(dwChar AS DWORD) AS DWORD
	SETSTATE DWORD Set.TimeSep dwChar

///
/// Return the setting that determines whether to include unique record keys in an order.
///
/// <returns>
/// </returns>

FUNCTION SetUnique() AS LOGIC
	GETSTATE LOGIC Set.Unique

///
/// Change the setting that determines whether to include unique record keys in an order.
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>

FUNCTION SetUnique(lSet AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.Unique lSet

///
///
/// <returns>
/// </returns>
FUNCTION SetYield() AS LOGIC
	GETSTATE LOGIC Set.Yield

///
///
/// <param name="lSet"></param>
/// <returns>
/// </returns>
FUNCTION SetYield(lSet AS LOGIC) AS LOGIC
	SETSTATE LOGIC Set.Yield lSet

///
/// Retrieve and set the X# return code.
///
/// <param name="dw"></param>
/// <returns>
/// </returns>
FUNCTION ErrorLevel(dw AS DWORD) AS DWORD
	SETSTATE DWORD Set.ErrorLevel dw

///
/// Retrieve the X# return code.
///
/// <returns>
/// </returns>
FUNCTION ErrorLevel() AS DWORD
	GETSTATE DWORD Set.ErrorLevel

///
/// Return the setting that determines the international mode for the application
///
/// <returns>The current setting, either "Windows" (the default) or  "Clipper"
/// </returns>
FUNCTION SetInternational() AS STRING
	RETURN RuntimeState.International:ToString():ToUpper()

///
/// Return and change the setting that determines the international mode for the application
///
// <param name="cMode">The collation mode to use. The available modes are "Windows" (the default) and "Clipper". "Unicode" and "Ordinal" can be used as synonym for "Windows".</param>
/// <returns>The current setting, either "Windows" (the default) or  "Clipper"
/// </returns>
/// <remarks>
/// SetInternational() allows XSharp apps to operate in different international modes.
/// The "CLIPPER" mode is provided for compatibility with CA-Clipper applications and uses an
/// internationalization routine defined in the nation module.

/// The "Windows" mode uses international services provided by Windows.
/// When you set this mode several settings will be changed
/// <list type="table">
/// <listheader>
/// <term>Setting</term> <description>Initial value in CLIPPER mode</description>
/// </listheader>
///	<item><term>SetAmExt</term> <description>Empty String</description></item>
/// <item><term>SetPmExt</term> <description>Empty String</description></item>
/// <item><term>SetAmPm</term> <description>FALSE (24 hour format)</description></item>
/// <item><term>SetCentury</term> <description>FALSE</description></item>
/// <item><term>SetDateCountry</term> <description>American (1)</description></item>
/// <item><term>SetDateFormat</term> <description>mm/dd/yy</description></item>
/// <item><term>SetDecimal</term> <description>2</description></item>
/// <item><term>SetDecimalSep</term> <description>Period (.)</description></item>
/// <item><term>SetThousandSep</term> <description>Comma (,)</description></item>
/// <item><term>SetTimeSep</term> <description>Colon(:)</description></item>
/// </list>
/// </remarks>
FUNCTION SetInternational(cMode AS STRING) AS STRING
	LOCAL cOld AS STRING
	cOld := RuntimeState.International:ToString():ToUpper()
	SWITCH cMode:ToUpper()
	CASE "CLIPPER"
		RuntimeState.GetInstance()._SetInternationalClipper()
	CASE "WINDOWS"
	CASE "UNICODE"
	CASE "ORDINAL"
		RuntimeState.GetInstance()._SetInternationalWindows()
	OTHERWISE
		THROW Error.ArgumentError(__ENTITY__, NAMEOF(cMode), "Unsupported international mode: "+ cMode)
	END SWITCH
	RETURN cOld

///
/// Return and change the setting that determines the internal collation routine used for string comparisons when running in the VO or Vulcan dialect.
/// The Core dialect always compares according to the Unicode rules.
/// The available modes are "Windows" (the default),  "Clipper", "Unicode" and "Ordinal".
///
/// <returns>The current setting for SetCollation().</returns>
/// <remarks>
/// <include file="CoreComments.xml" path="Comments/Collation/*" />
/// </remarks>
FUNCTION SetCollation() AS STRING
	RETURN RuntimeState.CollationMode:ToString():ToUpper()

///
/// Return and change the setting that determines the internal collation routine used for string comparisons when running in the VO or Vulcan dialect.
///
/// <remarks>
/// <include file="CoreComments.xml" path="Comments/Collation/*" />
/// </remarks>
/// <returns>The current setting, either "Windows" (the default),  "Clipper", "Unicode" or "Ordinal" </returns>
/// <param name="cCollation">The collation mode to use. The available modes are "Windows" (the default),  "Clipper", "Unicode" and "Ordinal". </param>
FUNCTION SetCollation(cCollation AS STRING)  AS STRING
	LOCAL cOld AS STRING
	cOld := RuntimeState.CollationMode:ToString():ToUpper()
	SWITCH cCollation:ToUpper()
	CASE "CLIPPER"
		RuntimeState.CollationMode := CollationMode.Clipper
	CASE "WINDOWS"
		RuntimeState.CollationMode := CollationMode.Windows
	CASE "UNICODE"
		RuntimeState.CollationMode := CollationMode.Unicode
	CASE "ORDINAL"
		RuntimeState.CollationMode := CollationMode.Ordinal
	OTHERWISE
		THROW Error.ArgumentError(__ENTITY__, NAMEOF(cCollation), "Unsupported collation mode: "+cCollation)
	END SWITCH
	RETURN cOld


Updated Test

Updated Test

// TestDbfNtx.prg
// Created by    : fabri
// Creation Date : 9/16/2018 4:17:53 PM
// Created for   :
// WorkStation   : FABPORTABLE

USING System
USING System.Collections.Generic
USING System.Text
USING Xunit
USING XSharp.RDD
USING XSharp.Rdd.Support
USING System.Diagnostics

BEGIN NAMESPACE XSharp.RDD.Tests

	/// <summary>
	/// The TestDbfNtx class.
	/// </summary>
	CLASS TestDbfNtx

		[Fact, Trait("DbfNtx", "Create")];
		METHOD CheckCreateDBFNtx() AS VOID
			LOCAL fieldDefs := "ID,N,5,0;NAME,C,20,0;MAN,L,1,0;BIRTHDAY,D,8,0" AS STRING
			LOCAL fields := fieldDefs:Split( ';' ) AS STRING[]
			VAR dbInfo := DbOpenInfo{ "TestNtx1.DBF", "TestNtx1", 1, FALSE, FALSE }
			//
			LOCAL myDBF AS DbfNtx
			LOCAL fieldInfo AS STRING[]
			LOCAL rddInfo AS RddFieldInfo[]
			rddInfo := RddFieldInfo[]{fields:Length}
			FOR VAR i := __ARRAYBASE__ TO fields:Length - (1-__ARRAYBASE__)
				//
				LOCAL currentField AS RddFieldInfo
				fieldInfo := fields[i]:Split( ',' )
				currentField := RddFieldInfo{ fieldInfo[DBS_NAME], fieldInfo[DBS_TYPE], Convert.ToInt32(fieldInfo[DBS_LEN]), Convert.ToInt32(fieldInfo[DBS_DEC]) }
				rddInfo[i] := currentField
			NEXT
			//
			myDBF := DbfNtx{}
			myDBF:SetFieldExtent( fields:Length )
			myDBF:CreateFields( rddInfo )
			// WE HAVE TO SET THE WORKAREA INFO !!!!
			dbInfo:WorkArea  := RuntimeState.Workareas:FindEmptyArea(TRUE)
			RuntimeState.Workareas:SetArea(dbInfo:WorkArea, myDBF)
			// Now Check
			Assert.Equal( TRUE, myDBF:Create( dbInfo ) )

			//
			LOCAL ntxInfo AS DbOrderCreateInfo
			ntxInfo := DbOrderCreateInfo{}
			ntxInfo:BagName := "TestNtx1"
			ntxInfo:Order := "TestNtx1"
			ntxInfo:Expression := "_FIELD->ID"
			//
			Assert.Equal( TRUE, myDBF:OrderCreate( ntxInfo ) )
			//
			myDBF:Close()
			RETURN

		[Fact, Trait("DbfNtx", "Open")];
		METHOD OpenDBFNtx() AS VOID
			// ID,N,5,0;NAME,C,20,0
			VAR dbInfo := DbOpenInfo{ "TestNTX2.DBF", "customer", 1, FALSE, FALSE }
			//
			LOCAL myDBF AS DbfNtx
			myDBF := DbfNtx{}
			// WE HAVE TO SET THE WORKAREA INFO !!!!
			LOCAL area  := 0    AS DWORD
			area  := RuntimeState.Workareas:FindEmptyArea(TRUE)
			RuntimeState.Workareas:SetArea(area, myDBF)
			RuntimeState.Workareas:CurrentWorkAreaNO := area
			dbInfo:WorkArea := area

			Assert.Equal( TRUE, myDBF:Open( dbInfo ) )
			//
			LOCAL ntxInfo AS DbOrderInfo
			ntxInfo := DbOrderInfo{}
			ntxInfo:BagName := "TestNTX2"
			ntxInfo:Order := "TestNTX2"
			// FilePath NullOrEmpty => Will get the FilePath of the DBF
			Assert.Equal( TRUE, myDBF:OrderListAdd( ntxInfo, "" ) )
			//
			myDBF:Close()
			RETURN

		[Fact, Trait("DbfNtx", "Value")];
		METHOD ReadDBFNtx() AS VOID
			// ID,N,5,0;NAME,C,20,0
			VAR dbInfo := DbOpenInfo{ "TestNTX2.DBF", "customer", 1, FALSE, FALSE }
			//
			LOCAL myDBF AS DbfNtx
			myDBF := DbfNtx{}
			// WE HAVE TO SET THE WORKAREA INFO !!!!
			LOCAL area  := 0    AS DWORD
			area  := RuntimeState.Workareas:FindEmptyArea(TRUE)
			RuntimeState.Workareas:SetArea(area, myDBF)
			RuntimeState.Workareas:CurrentWorkAreaNO := area
			dbInfo:WorkArea := area
			VAR success := myDBF:Open( dbInfo )
			Assert.Equal( TRUE, success )
			//
			LOCAL ntxInfo AS DbOrderInfo
			ntxInfo := DbOrderInfo{}
			ntxInfo:BagName := "TestNTX2"
			ntxInfo:Order := "TestNTX2"
			// FilePath NullOrEmpty => Will get the FilePath of the DBF
			Assert.Equal( TRUE, myDBF:OrderListAdd( ntxInfo, "" ) )
			//
			WHILE ! myDBF:EoF
				Debug.Write( "---===---" + Environment.NewLine )
				FOR VAR i := 1 TO myDBF:FIELDCount
					//
					LOCAL oData AS OBJECT
					oData := myDBF:GetValue( i )
					LOCAL str AS STRING
					str := oData:ToString()
					Debug.Write( str +" - " )
				NEXT
				Debug.Write( Environment.NewLine )
				myDBF:Skip(1)
			ENDDO
			//
			myDBF:Close()
			RETURN

	END CLASS
END NAMESPACE // XSharp.RDD.Tests

// TestDbfNtx.prg
// Created by    : fabri
// Creation Date : 9/16/2018 4:17:53 PM
// Created for   :
// WorkStation   : FABPORTABLE

USING System
USING System.Collections.Generic
USING System.Text
USING Xunit
USING XSharp.RDD
USING XSharp.Rdd.Support

BEGIN NAMESPACE XSharp.RDD.Tests

	/// <summary>
	/// The TestDbfNtx class.
	/// </summary>
	CLASS TestDbfNtx

		[Fact, Trait("DbfNtx", "Create")];
		METHOD CheckCreateDBFNtx() AS VOID
			LOCAL fieldDefs := "ID,N,5,0;NAME,C,20,0;MAN,L,1,0;BIRTHDAY,D,8,0" AS STRING
			LOCAL fields := fieldDefs:Split( ';' ) AS STRING[]
			VAR dbInfo := DbOpenInfo{ "TestNtx1.DBF", "TestNtx1", 1, FALSE, FALSE }
			//
			LOCAL myDBF AS DbfNtx
			LOCAL fieldInfo AS STRING[]
			LOCAL rddInfo AS RddFieldInfo[]
			rddInfo := RddFieldInfo[]{fields:Length}
			FOR VAR i := __ARRAYBASE__ TO fields:Length - (1-__ARRAYBASE__)
				//
				LOCAL currentField AS RddFieldInfo
				fieldInfo := fields[i]:Split( ',' )
				currentField := RddFieldInfo{ fieldInfo[DBS_NAME], fieldInfo[DBS_TYPE], Convert.ToInt32(fieldInfo[DBS_LEN]), Convert.ToInt32(fieldInfo[DBS_DEC]) }
				rddInfo[i] := currentField
			NEXT
			//
			myDBF := DbfNtx{}
			myDBF:SetFieldExtent( fields:Length )
			myDBF:CreateFields( rddInfo )
			// WE HAVE TO SET THE WORKAREA INFO !!!!
			dbInfo:WorkArea  := RuntimeState.Workareas:FindEmptyArea(TRUE)
			RuntimeState.Workareas:SetArea(dbInfo:WorkArea, myDBF)
			// Now Check
			Assert.Equal( TRUE, myDBF:Create( dbInfo ) )

			//
			LOCAL ntxInfo AS DbOrderCreateInfo
			ntxInfo := DbOrderCreateInfo{}
			ntxInfo:BagName := "TestNtx1"
			ntxInfo:Order := "TestNtx1"
			ntxInfo:Expression := "_FIELD->ID"
			//
			Assert.Equal( TRUE, myDBF:OrderCreate( ntxInfo ) )
			//
			myDBF:Close()
			RETURN

		[Fact, Trait("DbfNtx", "Open")];
		METHOD OpenDBFNtx() AS VOID
			// ID,N,5,0;NAME,C,20,0
			VAR dbInfo := DbOpenInfo{ "TestNTX2.DBF", "customer", 1, FALSE, FALSE }
			//
			LOCAL myDBF AS DbfNtx
			myDBF := DbfNtx{}
			// WE HAVE TO SET THE WORKAREA INFO !!!!
			dbInfo:WorkArea  := RuntimeState.Workareas:FindEmptyArea(TRUE)
			RuntimeState.Workareas:SetArea(dbInfo:WorkArea, myDBF)
			Assert.Equal( TRUE, myDBF:Open( dbInfo ) )
			//
			LOCAL ntxInfo AS DbOrderInfo
			ntxInfo := DbOrderInfo{}
			ntxInfo:BagName := "TestNTX2"
			ntxInfo:Order := "TestNTX2"
			// FilePath NullOrEmpty => Will get the FilePath of the DBF
			Assert.Equal( TRUE, myDBF:OrderListAdd( ntxInfo, "" ) )
			//
			myDBF:Close()
			RETURN

		[Fact, Trait("DbfNtx", "Value")];
		METHOD ReadDBFNtx() AS VOID
			// ID,N,5,0;NAME,C,20,0
			VAR dbInfo := DbOpenInfo{ "TestNTX2.DBF", "customer", 1, FALSE, FALSE }
			//
			LOCAL myDBF AS DbfNtx
			myDBF := DbfNtx{}
			// WE HAVE TO SET THE WORKAREA INFO !!!!
			dbInfo:WorkArea  := RuntimeState.Workareas:FindEmptyArea(TRUE)
			RuntimeState.Workareas:SetArea(dbInfo:WorkArea, myDBF)
			VAR success := myDBF:Open( dbInfo )
			Assert.Equal( TRUE, success )
			//
			LOCAL ntxInfo AS DbOrderInfo
			ntxInfo := DbOrderInfo{}
			ntxInfo:BagName := "TestNTX2"
			ntxInfo:Order := "TestNTX2"
			// FilePath NullOrEmpty => Will get the FilePath of the DBF
			Assert.Equal( TRUE, myDBF:OrderListAdd( ntxInfo, "" ) )
			//
			WHILE ! myDBF:EoF
				FOR VAR i := 1 TO myDBF:FIELDCount
					//
					LOCAL oData AS OBJECT
					oData := myDBF:GetValue( i )
					LOCAL str AS STRING
					str := oData:ToString()
				NEXT
				myDBF:Skip(1)
			ENDDO
			//
			myDBF:Close()
			RETURN

	END CLASS
END NAMESPACE // XSharp.RDD.Tests


[Runtime] Implemented the VFP MkDir() function

//
// 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.IO

// VFP standard flags
DEFINE S2F_FLAG_OVERWRITE           = 0x0000
DEFINE S2F_FLAG_APPEND              = 0x0001
DEFINE S2F_FLAG_UNICODE_LE          = 0x0002
DEFINE S2F_FLAG_UTF8                = 0x0004
// X# extension flags
DEFINE S2F_FLAG_UNICODE_BE          = 0x0008
DEFINE S2F_FLAG_UNICODE_FORMATS     = S2F_FLAG_UNICODE_LE | S2F_FLAG_UTF8 | S2F_FLAG_UNICODE_BE
DEFINE S2F_FLAG_UNICODE_TEXT        = 0x0100

/// <include file="VfpRuntimeDocs.xml" path="Runtimefunctions/mkdir/*" />
FUNCTION MkDir(cPath AS STRING) AS INT
    LOCAL nRet AS INT
    IF System.IO.Directory.Exists(cPath)
        nRet := 6
    ELSE
        TRY
            System.IO.Directory.CreateDirectory(cPath)
            nRet := 0
        CATCH
            nRet := 1
        END TRY
    END IF
RETURN nRet

/// <include file="VfpRuntimeDocs.xml" path="Runtimefunctions/strtofile/*" />
FUNCTION StrToFile (cExpression AS STRING, cFileName AS STRING) AS INT
    RETURN StrToFile(cExpression, cFileName, S2F_FLAG_OVERWRITE)

/// <include file="VfpRuntimeDocs.xml" path="Runtimefunctions/strtofile/*" />
FUNCTION StrToFile (cExpression AS STRING, cFileName AS STRING, lAdditive AS Boolean) AS INT
    RETURN StrToFile(cExpression, cFileName, IIF(lAdditive, S2F_FLAG_APPEND, S2F_FLAG_OVERWRITE))

/// <include file="VfpRuntimeDocs.xml" path="Runtimefunctions/strtofile/*" />
FUNCTION StrToFile (cExpression AS STRING, cFileName AS STRING, nFlags AS INT) AS INT
    LOCAL Additive := .F. AS LOGIC
    LOCAL BOM := "" AS STRING
    LOCAL Result := 0 AS DWORD
    LOCAL FHandle AS IntPtr
    LOCAL VFPBehavior := .T. AS LOGIC      // it means the string must hold an already prepared buffer, or it is binary
    LOCAL UnicodeEncoding AS System.Text.Encoding
    LOCAL IOError AS Exception
    UnicodeEncoding := System.Text.Encoding.Default
    SWITCH nFlags
    CASE S2F_FLAG_OVERWRITE
        // Just overwrite the file.
        Additive := .F.
    CASE S2F_FLAG_APPEND
        // VFP behavior, append to a file with no Unicode encoding
        Additive := .T.

    CASE S2F_FLAG_UNICODE_LE
        // VFP behavior, create a Unicode file (source buffer already prepared)
        BOM := e"\xFF\xFE"

    CASE S2F_FLAG_UTF8
        // VFP behavior, create a UTF-8 file (source buffer already prepared)
        BOM := e"\xEF\xBB\xBF"

    CASE S2F_FLAG_UNICODE_BE
        // extension to VFP behavior, create a Unicode Big-Endian file (source buffer already prepared)
        BOM := e"\xFE\xFF"

    OTHERWISE
        // if not simply overwrite, the file will be treated as a Unicode text file

        IF _AND(nFlags , S2F_FLAG_UNICODE_TEXT) != 0

            VFPBehavior := .F.
            Additive    := _AND(nFlags , S2F_FLAG_APPEND) != 0

            // set the Unicode encoding
            SWITCH _AND(nFlags,S2F_FLAG_UNICODE_FORMATS)
            CASE S2F_FLAG_UNICODE_LE
                UnicodeEncoding := System.Text.Encoding.Unicode

            CASE S2F_FLAG_UTF8
                UnicodeEncoding := System.Text.Encoding.UTF8

            CASE S2F_FLAG_UNICODE_BE
                UnicodeEncoding := System.Text.Encoding.BigEndianUnicode

            OTHERWISE
                // error if Unicode encoding not properly set
                THROW ArgumentException{"Incorrect Encoding Parameter",nameof(nFlags)}

            END SWITCH
        ELSE
            THROW ArgumentException{"Incorrect Parameter",nameof(nFlags)}
        ENDIF
    END SWITCH

    * append mode?
    IF Additive

        IF VFPBehavior

            * open an existing file, or create if it does not exists
            FHandle := FOpen(cFileName, FO_READWRITE + FO_SHARED)
            IF FHandle != F_ERROR
                * try to move to the end of the file
                FSeek3(FHandle, 0, FS_END)
                * and write the contents of the buffer
                IF FError() == 0
                    Result := FWrite(FHandle, cExpression, (DWORD) cExpression:Length)
                ENDIF
                * if everything went ok, close the file handle
                IF FError() == 0
                        FClose(FHandle)
                        IF FError() != 0
                            THROW FException()
                        ENDIF
                    * if not, before throwing the exception...
                ELSE
                    IOError := FException()
                    FClose(FHandle)         // ... try to close the handle, anyway
                    THROW IOError
                ENDIF
            ELSE
                THROW FException()
            ENDIF
        ELSE
            * in non-VFP behavior, just append the Unicode string to an existing file
            TRY
                File.AppendAllText(cFileName, cExpression, UnicodeEncoding)
            CATCH
                THROW
            END TRY

            Result := (DWORD) cExpression:Length

        ENDIF

    ELSE

        * TO-DO: check on SET("Safety")

        * create a new file
        IF VFPBehavior

            * get an handle for a new file
            FHandle := FCreate(cFileName)
            IF FHandle != F_ERROR
                * start with the BOM of the file
                IF !String.IsNullOrEmpty(BOM)
                    Result = FWrite(FHandle, BOM, (DWORD) BOM:Length)
                ENDIF
                * try to write the contents from the buffer
                IF FError() == 0
                    Result += FWrite(FHandle, cExpression, (DWORD) cExpression:Length)
                ENDIF
                * and close the file
                IF FError() == 0
                    FClose(FHandle)
                    IF FError() != 0
                        THROW FException()
                    ENDIF
                    * if an error occurred...
                ELSE
                    IOError = FException()
                    FClose(FHandle)     // ... try to not leave the handle open, in any case
                    THROW IOError
                ENDIF
            ELSE
                THROW FException()
            ENDIF

        ELSE

            * write the Unicode string to a text file
            TRY
                File.WriteAllText(cFileName, cExpression, UnicodeEncoding)
            CATCH
                THROW
            END TRY
            Result := (DWORD) cExpression:Length
        ENDIF

    ENDIF

    * return the length of bytes / characters
    RETURN (INT) Result

    END FUNCTION

/// <include file="VfpRuntimeDocs.xml" path="Runtimefunctions/filetostr/*" />
FUNCTION FileToStr (cFileName AS STRING) AS STRING
    RETURN FileToStr(cFileName, 0)

/// <include file="VfpRuntimeDocs.xml" path="Runtimefunctions/filetostr/*" />
FUNCTION FileToStr (cFileName AS STRING, Flags AS INT) AS STRING
    LOCAL FHandle AS IntPtr
    LOCAL StrLen AS DWORD
    LOCAL Result := "" AS STRING
    LOCAL IOError AS Exception

    IF _AND(Flags, S2F_FLAG_UNICODE_TEXT) == 0      // VFP behavior, read file as binary, even if it is a Unicode text

        IF File(cFileName)
            cFileName := FPathName()
        ENDIF
        FHandle := FOpen(cFileName, FO_READ + FO_SHARED)
        IF FHandle != F_ERROR
                * success opening the file, get its size...
                StrLen := (DWORD) FSize(FHandle)
                IF StrLen > 0
                    * ...and allocate a buffer for it
                    Result := Space(StrLen)
                    IF FRead(FHandle, @Result, StrLen) != StrLen
                        Result := ""
                    ENDIF
                ELSE
                    Result := ""
                ENDIF
                * any IO error will throw an exception
                IF FError() != 0
                        IOError := FException()
                        FClose(FHandle)
                    THROW IOError
                ELSE
                    FClose(FHandle)
            ENDIF
        ELSE
            THROW FException()
        ENDIF

    ELSE

        * for a Unicode text file, just read it into a string
        TRY
            Result := File.ReadAllText(cFileName)     // read a text file
        CATCH
            THROW
        END TRY

    ENDIF

    * return the contents of the file
    RETURN Result

    END FUNCTION

//
// 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.IO

// VFP standard flags
DEFINE S2F_FLAG_OVERWRITE           = 0x0000
DEFINE S2F_FLAG_APPEND              = 0x0001
DEFINE S2F_FLAG_UNICODE_LE          = 0x0002
DEFINE S2F_FLAG_UTF8                = 0x0004
// X# extension flags
DEFINE S2F_FLAG_UNICODE_BE          = 0x0008
DEFINE S2F_FLAG_UNICODE_FORMATS     = S2F_FLAG_UNICODE_LE | S2F_FLAG_UTF8 | S2F_FLAG_UNICODE_BE
DEFINE S2F_FLAG_UNICODE_TEXT        = 0x0100

/// <include file="VfpRuntimeDocs.xml" path="Runtimefunctions/strtofile/*" />
FUNCTION StrToFile (cExpression AS STRING, cFileName AS STRING) AS INT
    RETURN StrToFile(cExpression, cFileName, S2F_FLAG_OVERWRITE)

/// <include file="VfpRuntimeDocs.xml" path="Runtimefunctions/strtofile/*" />
FUNCTION StrToFile (cExpression AS STRING, cFileName AS STRING, lAdditive AS Boolean) AS INT
    RETURN StrToFile(cExpression, cFileName, IIF(lAdditive, S2F_FLAG_APPEND, S2F_FLAG_OVERWRITE))

/// <include file="VfpRuntimeDocs.xml" path="Runtimefunctions/strtofile/*" />
FUNCTION StrToFile (cExpression AS STRING, cFileName AS STRING, nFlags AS INT) AS INT
    LOCAL Additive := .F. AS LOGIC
    LOCAL BOM := "" AS STRING
    LOCAL Result := 0 AS DWORD
    LOCAL FHandle AS IntPtr
    LOCAL VFPBehavior := .T. AS LOGIC      // it means the string must hold an already prepared buffer, or it is binary
    LOCAL UnicodeEncoding AS System.Text.Encoding
    LOCAL IOError AS Exception
    UnicodeEncoding := System.Text.Encoding.Default
    SWITCH nFlags
    CASE S2F_FLAG_OVERWRITE
        // Just overwrite the file.
        Additive := .F.
    CASE S2F_FLAG_APPEND
        // VFP behavior, append to a file with no Unicode encoding
        Additive := .T.

    CASE S2F_FLAG_UNICODE_LE
        // VFP behavior, create a Unicode file (source buffer already prepared)
        BOM := e"\xFF\xFE"

    CASE S2F_FLAG_UTF8
        // VFP behavior, create a UTF-8 file (source buffer already prepared)
        BOM := e"\xEF\xBB\xBF"

    CASE S2F_FLAG_UNICODE_BE
        // extension to VFP behavior, create a Unicode Big-Endian file (source buffer already prepared)
        BOM := e"\xFE\xFF"

    OTHERWISE
        // if not simply overwrite, the file will be treated as a Unicode text file

        IF _AND(nFlags , S2F_FLAG_UNICODE_TEXT) != 0

            VFPBehavior := .F.
            Additive    := _AND(nFlags , S2F_FLAG_APPEND) != 0

            // set the Unicode encoding
            SWITCH _AND(nFlags,S2F_FLAG_UNICODE_FORMATS)
            CASE S2F_FLAG_UNICODE_LE
                UnicodeEncoding := System.Text.Encoding.Unicode

            CASE S2F_FLAG_UTF8
                UnicodeEncoding := System.Text.Encoding.UTF8

            CASE S2F_FLAG_UNICODE_BE
                UnicodeEncoding := System.Text.Encoding.BigEndianUnicode

            OTHERWISE
                // error if Unicode encoding not properly set
                THROW ArgumentException{"Incorrect Encoding Parameter",nameof(nFlags)}

            END SWITCH
        ELSE
            THROW ArgumentException{"Incorrect Parameter",nameof(nFlags)}
        ENDIF
    END SWITCH

    * append mode?
    IF Additive

        IF VFPBehavior

            * open an existing file, or create if it does not exists
            FHandle := FOpen(cFileName, FO_READWRITE + FO_SHARED)
            IF FHandle != F_ERROR
                * try to move to the end of the file
                FSeek3(FHandle, 0, FS_END)
                * and write the contents of the buffer
                IF FError() == 0
                    Result := FWrite(FHandle, cExpression, (DWORD) cExpression:Length)
                ENDIF
                * if everything went ok, close the file handle
                IF FError() == 0
                        FClose(FHandle)
                        IF FError() != 0
                            THROW FException()
                        ENDIF
                    * if not, before throwing the exception...
                ELSE
                    IOError := FException()
                    FClose(FHandle)         // ... try to close the handle, anyway
                    THROW IOError
                ENDIF
            ELSE
                THROW FException()
            ENDIF
        ELSE
            * in non-VFP behavior, just append the Unicode string to an existing file
            TRY
                File.AppendAllText(cFileName, cExpression, UnicodeEncoding)
            CATCH
                THROW
            END TRY

            Result := (DWORD) cExpression:Length

        ENDIF

    ELSE

        * TO-DO: check on SET("Safety")

        * create a new file
        IF VFPBehavior

            * get an handle for a new file
            FHandle := FCreate(cFileName)
            IF FHandle != F_ERROR
                * start with the BOM of the file
                IF !String.IsNullOrEmpty(BOM)
                    Result = FWrite(FHandle, BOM, (DWORD) BOM:Length)
                ENDIF
                * try to write the contents from the buffer
                IF FError() == 0
                    Result += FWrite(FHandle, cExpression, (DWORD) cExpression:Length)
                ENDIF
                * and close the file
                IF FError() == 0
                    FClose(FHandle)
                    IF FError() != 0
                        THROW FException()
                    ENDIF
                    * if an error occurred...
                ELSE
                    IOError = FException()
                    FClose(FHandle)     // ... try to not leave the handle open, in any case
                    THROW IOError
                ENDIF
            ELSE
                THROW FException()
            ENDIF

        ELSE

            * write the Unicode string to a text file
            TRY
                File.WriteAllText(cFileName, cExpression, UnicodeEncoding)
            CATCH
                THROW
            END TRY
            Result := (DWORD) cExpression:Length
        ENDIF

    ENDIF

    * return the length of bytes / characters
    RETURN (INT) Result

    END FUNCTION

/// <include file="VfpRuntimeDocs.xml" path="Runtimefunctions/filetostr/*" />
FUNCTION FileToStr (cFileName AS STRING) AS STRING
    RETURN FileToStr(cFileName, 0)

/// <include file="VfpRuntimeDocs.xml" path="Runtimefunctions/filetostr/*" />
FUNCTION FileToStr (cFileName AS STRING, Flags AS INT) AS STRING
    LOCAL FHandle AS IntPtr
    LOCAL StrLen AS DWORD
    LOCAL Result := "" AS STRING
    LOCAL IOError AS Exception

    IF _AND(Flags, S2F_FLAG_UNICODE_TEXT) == 0      // VFP behavior, read file as binary, even if it is a Unicode text

        IF File(cFileName)
            cFileName := FPathName()
        ENDIF
        FHandle := FOpen(cFileName, FO_READ + FO_SHARED)
        IF FHandle != F_ERROR
                * success opening the file, get its size...
                StrLen := (DWORD) FSize(FHandle)
                IF StrLen > 0
                    * ...and allocate a buffer for it
                    Result := Space(StrLen)
                    IF FRead(FHandle, @Result, StrLen) != StrLen
                        Result := ""
                    ENDIF
                ELSE
                    Result := ""
                ENDIF
                * any IO error will throw an exception
                IF FError() != 0
                        IOError := FException()
                        FClose(FHandle)
                    THROW IOError
                ELSE
                    FClose(FHandle)
            ENDIF
        ELSE
            THROW FException()
        ENDIF

    ELSE

        * for a Unicode text file, just read it into a string
        TRY
            Result := File.ReadAllText(cFileName)     // read a text file
        CATCH
            THROW
        END TRY

    ENDIF

    * return the contents of the file
    RETURN Result

    END FUNCTION