StrToFile ( cExpression, cFileName [, lAdditive | nFlag])

This forum is meant for questions about the Visual FoxPro Language support in X#.

Karl-Heinz
Posts: 774
Joined: Wed May 17, 2017 8:50 am
Location: Germany

StrToFile ( cExpression, cFileName [, lAdditive | nFlag])

Post by Karl-Heinz »

Hi Antonio,

You can throw a exception in this way, assuming you change the Flag type from INT to DWORD - otherwise you must also check for values < 0

Code: Select all

IF Flag == 3 .OR. Flag > 4 
		
	// valid Flag values are 0,1,2 or 4

	THROW ArgumentException { String.Format("Param value is {0}, but it must be either 0,1,2 or 4", Flag ) , NAMEOF ( Flag )  }

ENDIF 
regards
Karl-Heinz
atlopes
Posts: 83
Joined: Sat Sep 07, 2019 11:43 am
Location: Portugal

StrToFile ( cExpression, cFileName [, lAdditive | nFlag])

Post by atlopes »

Chris, Robert, and Karl-Heinz

Thank you for all the replies; your points were taken.

Regarding error handling, what I was looking specifically for is the defined list of exceptions in the X# namespaces, if there are any, so as not to derive redundant exception classes.

As for the encoding, Robert, the suggested implementation tries to mimic the VFP behavior. As I said above in this thread, the STRTOFILE() must not touch the string contents that are assumed to being binary in nature.

So, this should be possible, and the result should be an exact bit-by-bit copy of the original file.

Code: Select all

    pngFileName = "<some PNG file>"
    pngImage = FILETOSTR(pngFilename)
    STRTOFILE(pngImage, JUSTPATH(pngFilename) + "Copy of " + JUSTFNAME(pngFilename), 0)
This works in VFP, should also work in X# as it is (we still have the Flags parameter to enhance the features of the function, including the ability to encode the string according to some Unicode format). As far as I tested, the implementation is producing the copy as required, but I might not have submitted the results to proper stress tests.
User avatar
robert
Posts: 4529
Joined: Fri Aug 21, 2015 10:57 am
Location: Netherlands

StrToFile ( cExpression, cFileName [, lAdditive | nFlag])

Post by robert »

Antonio,
If you want to make an exact copy of a binary file in a unicode environment the best you can do is to read the file as a list of bytes and then write it as a list of bytes as well.
https://docs.microsoft.com/en-us/dotnet ... adallbytes
https://docs.microsoft.com/en-us/dotnet ... teallbytes

Robert
XSharp Development Team
The Netherlands
robert@xsharp.eu
atlopes
Posts: 83
Joined: Sat Sep 07, 2019 11:43 am
Location: Portugal

StrToFile ( cExpression, cFileName [, lAdditive | nFlag])

Post by atlopes »

Robert, I don't want to make an exact copy of a binary file as a use case, but such capability illustrates the behavior of the VFP FILETOSTR() and STRTOFILE() functions. If you're targeting VFP compatibility, I assume you would want to see the behavior replicated (mainly because the functions are not part of other vocabularies).

As for reading and writing raw data files, I thought that XSharp.Core.FRead() and FWrite() would provide the basis for the functionality (and I would guess that they are calling the relevant .Net methods). In fact, there are no conceptual differences between the implementation I suggested and the examples to be found in the FWrite() documentation.

https://www.xsharp.eu/runtimehelp/html/ ... FWrite.htm
User avatar
robert
Posts: 4529
Joined: Fri Aug 21, 2015 10:57 am
Location: Netherlands

StrToFile ( cExpression, cFileName [, lAdditive | nFlag])

Post by robert »

Antonio,
I understand what you are trying to do.
However when you use Fread() and FWrite() with "buffers" of type STRING then you are introducing a complexity in a unicode environment: the runtime can't know if you expect the data in the files to be of type STRING or of type BINARY. And when the type is STRING it needs to know if you expect the runtime to convert to/from Unicode.
Many people (at least in the VO side of XBase) have used functions such as FRead, FWrite, FreadLine() and FWriteLine() to read/write Ansi text files. They expect that the runtime does an automatic conversion from the Ansi text to Unicode.
If you look at the FoxPro example for FREAD you will also see that it is used for reading TEXT files:

Code: Select all

Local gnFileHandle,nSize,cString
gnFileHandle = FOPEN("test.txt")
* Seek to end of file to determine number of bytes in the file.
nSize =  FSEEK(gnFileHandle, 0, 2)     && Move pointer to EOF
IF nSize <= 0
 * If file is empty, display an error message.
 WAIT WINDOW "This file is empty!" NOWAIT
ELSE
 * If file is not empty, store the file's contents in memory
 * and display the text in the main Visual FoxPro window.
 = FSEEK(gnFileHandle, 0, 0)      && Move pointer to BOF
 cString = FREAD(gnFileHandle, nSize)
 ? cString
ENDIF
= FCLOSE(gnFileHandle)         && Close the file
So this code also has to convert Ansi text to Unicode.
So what I would advise is:
- To read TEXT files, use a buffer of type string and use FRead() and FWrite()
- to read Binary files allocate a buffer with MemAlloc or use an array of bytes and use Fread3() and FWrite3()


Robert
XSharp Development Team
The Netherlands
robert@xsharp.eu
atlopes
Posts: 83
Joined: Sat Sep 07, 2019 11:43 am
Location: Portugal

StrToFile ( cExpression, cFileName [, lAdditive | nFlag])

Post by atlopes »

Robert,

The STRTOFILE() and FILETOSTR() functions have only to make sure to send the contents of the string to the file, and vice-versa. Of course, when the string system is single-byte that is fairly simple to accomplish (the example you presented could use a "test.gif" file, in VFP it wouldn't make a difference). It's not that simple when the character system moves to Unicode, as you pointed out, and to which I agree.

I propose to extend the functionalities of both functions by adding other flags. In this way, the VFP behavior can still be fully supported, as well as more sensible options to easily write and read Unicode text files.

I combined the code from Kar-Heinz with mine - if that is ok. Other issues must still be addressed down the road, like NULL handling, but I think that this is a fairly regular VFP-compatible implementation of the two functions that also introduces new features on the side.

Code: Select all

* 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

FUNCTION StrToFile (Expression AS String, Filename AS String, Flags AS Int) AS Int
    
    LOCAL Additive = .F. AS Boolean
    LOCAL BOM = "" AS String
    LOCAL Result = 0 AS Int
    LOCAL FHandle AS Int
    LOCAL VFPBehavior = .T. AS Boolean      // it means the string must hold an already prepared buffer, or it is binary
    LOCAL UnicodeEncoding AS System.Text.Encoding

    DO CASE
        CASE Flags = S2F_FLAG_APPEND
            Additive = .T.
            
        CASE Flags = S2F_FLAG_UNICODE_LE
            BOM = e"xFFxFE"

        CASE Flags = S2F_FLAG_UTF8
            BOM = e"xEFxBBxBF"

        CASE Flags = S2F_FLAG_UNICODE_BE
            BOM = e"xFExFF"
            
        CASE Flags != S2F_FLAG_OVERWRITE

            IF (Flags & S2F_FLAG_UNICODE_TEXT) != 0

                VFPBehavior = .F.
                
                Additive = (Flags & S2F_FLAG_APPEND) != 0
            
                SWITCH Flags & 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
                        THROW ArgumentException {}
                        
                END SWITCH
            ELSE
                THROW ArgumentException {}
            ENDIF
    END CASE
    
    IF Additive

        IF VFPBehavior

            FHandle = FOpen(Filename, FO_READWRITE + FO_SHARED)
            IF FHandle != F_ERROR
                FSeek3(FHandle, 0, FS_END)
                Result = FWrite(FHandle, Expression)
                FClose(FHandle)
            ENDIF
            
        ELSE
            
            TRY
                File.AppendAllText(Filename, Expression, UnicodeEncoding)
            CATCH
                THROW
            ENDTRY
                
            Result = Expression:Length
            
        ENDIF

    ELSE
        
        IF VFPBehavior
        
            FHandle = FCreate(Filename)
            IF FHandle != F_ERROR
                IF ! (BOM == "")
                    Result = FWrite(FHandle, BOM)
                ENDIF
                Result += FWrite(FHandle, Expression)
                FClose(FHandle)
            ENDIF
 
        ELSE
            
            TRY
                File.WriteAllText(Filename, Expression, UnicodeEncoding)
            CATCH
                THROW
            ENDTRY
            
            Result = Expression:Length
            
        ENDIF

    ENDIF

    RETURN Result

ENDFUNC

FUNCTION FileToStr (Filename AS String) AS String
    RETURN FileToStr(Filename, 0)

FUNCTION FileToStr (Filename AS String, Flags AS Int) AS String
    
    LOCAL FHandle AS Int
    LOCAL StrLen AS Int
    LOCAL Result = .NULL. AS String

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

        FHandle = FOpen(Filename, FO_READ + FO_SHARED)
        IF FHandle != F_ERROR
            StrLen = FSize(FHandle)
            IF StrLen > 0
                Result = SPACE(StrLen)
                IF FRead(FHandle, @Result, StrLen) != StrLen
                    Result = .NULL.
                ENDIF
            ELSE
                Result = ""
            ENDIF
        ENDIF

    ELSE
        
        TRY
            Result = File.ReadAllText(Filename)     // read a text file
        CATCH
            THROW
        ENDTRY
        
    ENDIF

    RETURN Result
    
ENDFUNC
Karl-Heinz
Posts: 774
Joined: Wed May 17, 2017 8:50 am
Location: Germany

StrToFile ( cExpression, cFileName [, lAdditive | nFlag])

Post by Karl-Heinz »

Hi Antonio,

i think your defines should be declared in this way

Code: Select all

* 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  
I can´t comment in which situation VFP throws an runtime error or surpresses the error and sets maybe an VFP internal errorcode instead ? , but when i look at the places where you currently want to throw an error you should change that to:

Code: Select all

THROW ArgumentException { String.Format("Param value {0} is invalid" , Flags ) , NAMEOF ( Flags ) }
and

Code: Select all

TRY

  File.AppendAllText(Filename, Expression, UnicodeEncoding)

CATCH e AS Exception 

  THROW e

ENDTRY
X# File functions like Fcreate() don´t throw an runtime error. If such a func fails the dos errorcode and the *exception* is stored in the RuntimeState object. When i run this code your function returns 0, but i´m able to see why Fcreate() failed:

Code: Select all

 
? StrToFile ( "Drive 'P' doesn´t exist !" , "P:test.txt" , S2F_FLAG_UTF8  )  

IF RuntimeState.FileError > 0
	? RuntimeState.FileError , DosErrString ( RuntimeState.FileError )	
    ?
	IF RuntimeState.FileException != NULL
		THROW RuntimeState.FileException
	ENDIF	
ENDIF 
i see the errorcode 3, the errorcode description and the exception. This exception could also be thrown within your StrToFile() function e.g.

Code: Select all

 
...
       FHandle = FCreate(Filename)
           IF FHandle != F_ERROR
               IF ! (BOM == "")
                   Result = FWrite(FHandle, BOM)
               ENDIF
               Result += FWrite(FHandle, Expression)
               FClose(FHandle)
           ENDIF

	   IF RuntimeState.FileError > 0 .AND. RuntimeState.FileException != NULL 
		THROW RuntimeState.FileException
	   ENDIF 	
...									
All depends on what your VFP does if an invalid param is used or a file operation fails.

P.S. Forgot to mention FError(). This function returns the current RuntimeState.FileError value.

regards
Karl-Heinz
atlopes
Posts: 83
Joined: Sat Sep 07, 2019 11:43 am
Location: Portugal

StrToFile ( cExpression, cFileName [, lAdditive | nFlag])

Post by atlopes »

Karl-Heinz, thank you for your remarks.
i think your defines should be declared in this way

Code: Select all

* VFP standard flags
DEFINE S2F_FLAG_OVERWRITE       = 0x0000 
Ok, but why? (seriously, I'm trying to learn as much as I can from these interactions).
when i look at the places where you currently want to throw an error you should change that to:

Code: Select all

THROW ArgumentException { String.Format("Param value {0} is invalid" , Flags ) , NAMEOF ( Flags ) }
Wouldn't it be more appropriate to have these error messages centralized somewhere? Things like i18n will be harder to accomplish another way, and it was also for that I have asked about X# error handling guidelines.
and

Code: Select all

TRY

  File.AppendAllText(Filename, Expression, UnicodeEncoding)

CATCH e AS Exception 

  THROW e

ENDTRY
Got the less verbose (and more abstract) syntax from X# documentation. Any reason why your suggestion must replace the argument-less THROW?

Once again, thank you for these and the other remarks. They surely are giving me a better understanding of X#.
Karl-Heinz
Posts: 774
Joined: Wed May 17, 2017 8:50 am
Location: Germany

StrToFile ( cExpression, cFileName [, lAdditive | nFlag])

Post by Karl-Heinz »

Hi Antonio,

>> DEFINE vs. #DEFINE

when you look at e.g. the FCreate() help notes, you´ll see that the second param supports constants like FC_READONLY, which is defined in the https://github.com/X-Sharp/XSharpPublic ... le.prg#L85 as:

Code: Select all

DEFINE FC_READONLY   := 0x00000001  
This is translated to a public const FC_READONLY. Such a constant is publicly visible, while e.g. your #DEFINE S2F_FLAG_UTF8 is only visible in the files in which the #DEFINE S2F_FLAG_UTF8 is included.


Wouldn't it be more appropriate to have these error messages centralized somewhere?
As I already mentioned earlier, first of all you need to know *exactly* what VFP does when invalid params are used or file operations fail. What about the SET SAFETY check if the file to create already exists ?


Got the less verbose (and more abstract) syntax from X# documentation. Any reason why your suggestion must replace the argument-less THROW?
If an error happens i want to know what went wrong, and such information is stored in the "CATCH e" exception object. Whether the exception is forwarded with "THROW e" or an errorcode is set instead depends on your requirements.

regards
Karl-Heinz
atlopes
Posts: 83
Joined: Sat Sep 07, 2019 11:43 am
Location: Portugal

StrToFile ( cExpression, cFileName [, lAdditive | nFlag])

Post by atlopes »

Karl-Heinz, thank you again.
Such a constant is publicly visible, while e.g. your #DEFINE S2F_FLAG_UTF8 is only visible in the files in which the #DEFINE S2F_FLAG_UTF8 is included.
Got it. Really important to know.
Wouldn't it be more appropriate to have these error messages centralized somewhere?
As I already mentioned earlier, first of all you need to know *exactly* what VFP does when invalid params are used or file operations fail.
I think we're addressing different issues, here. What I said was that I wouldn't feel too comfortable to have error messages as literal strings spreading around the code. That would not be beneficial to localization and style consistency. So, THROW SomeException {} instead of THROW SomeException { "Error message" }.

As for the need to replicate as much as possible the VFP behavior, I totally agree with you.
If an error happens i want to know what went wrong, and such information is stored in the "CATCH e" exception object.
As per the help file,
Using THROW within a CATCH block without any arguments re-throws the exception, passing it unchanged to the next highest TRY-CATCH block.
Am I wrongly interpreting this as setting the exception is redundant? Or does this have any side-effect that I'm missing?
Post Reply