Steven Hobbs · Jun 29, 2022 go to post

Although the above description says $ORDER scans "down" a multidimensional global, other programers might say it scans "sideways".  There are many different structures for databases.  E.g., there are network databases (sometimes called CODASYL databases); there are hierarchical databases (like ObjectScript multidimensional arrays/globals); there are relational databases (often accessed by the SQL language); ...

ObjectScript is based on the ANSI M language standard.  I believe that the name of the ANSI M hierarchical function $QUERY has always been $QUERY but the original name of the ANSI M hierarchical function $ORDER was formerly $NEXT.  $NEXT is very similar to $ORDER but $NEXT had problems with its starting/ending subscript values.  IRIS ObjectScript no longer documents the obsolete $NEXT function but the ObjectScript compiler still accepts programs using $NEXT for backwards compatibility.

Consider the following ObjectScript global array:

USER>ZWRITE ^g
^g("a")="a"
^g("a",1)="a1"
^g("b",1)="b1"
^g("b",1,"c")="b1c"
^g("c")="c"
^g("c","b10")="cb10"
^g("c","b2")="cb2"
^g("d",2)="d2"
^g("d",10)="d10"
Consider the following walk sideways by $ORDER along the first subscript of ^g"

USER>set s=""
USER>for { SET s=$ORDER(^g(s))  QUIT:s=""  WRITE $NAME(^g(s)),! }    
^g("a")
^g("b")
^g("c")
^g("d")

Although these 4 globals contain values below them, the $ORDER walks did not walk down to deeper subscripts.  As it walked sideways, it returned the subscripts "b" and "d" even though ^g("b") and ^g("d") did not have values of their own but only values underneath them.

Now consider the walk down deeper by $QUERY through all the subscripts of ^g(...) at all subscript levels:

USER>set s="^g"
USER>for { WRITE s,!  SET s=$QUERY(@s)  QUIT:s="" }               
^g
^g("a")
^g("a",1)
^g("b",1)
^g("b",1,"c")
^g("c")
^g("c","b10")
^g("c","b2")
^g("d",2)
^g("d",10)

This walk by $QUERY was told to start at ^g and every call on $QUERY went through every subscripted node of ^g(...) that contained a value regardless of the number of subscripts needed.  However, elements ^g("b") and ^g("d") that did not contain values of their own were skipped by the $QUERY walk as it continued on to nodes with deeper subscripts that did contain values.

Also note that each $ORDER evaluation returned only a single subscript value as it walked sideways while each $QUERY evaluation returned a string containing the variable name along with all the subscript values of that particular multidimensional array node.

Steven Hobbs · May 27, 2022 go to post

The ZZDUMP command is useful for debugging, especially when a searching for a bug caused by an unprintable character in an ObjectScript string.  The most recent versions of the ZWRITE command are now usually easier to use than the ZZDUMP command when they are used for debugging purposes.

One of the legacy uses for ZZDUMP was to look at corrupted strings, especially when a $LIST string had become corrupted by the use of some non-$LIST compliant string operations.  Back when I first joined InterSystems the documentation of ZZDUMP included a description of some of the $LIST formats so programmers could use ZZDUMP to see if a $LIST string was corrupted.  However, now ZWRITE will use $LB(...) syntax when a string contains a valid $LIST and it will use $C(i,j,k,l,...) in other cases when a string contains unprintable characters.  This makes ZWRITE a much better command than ZZDUMP when checking to see if a $LIST string is corrupted.  ZWRITE can recognize a compressed $BIT string but I have very little experience with how compressed $BIT strings are encoded.  Also ZWRITE can provide a translation of a %Status value.  ZWRITE will change further in the future as we discover additional useful display formats.

Back when ZZDUMP was documenting $LIST format that caused two types of problems:  (1) When new $LIST encodings were added there were complaints that older applications did not understand the new encodings; and (2) When the documentation described multiple encodings that could be used for the same data then the documentation did not include the complicated details about which encodings could be generated and which encodings could not be generated.  In general, when new $LIST encodings were generated as a replacement for older encodings then all the $LIST functions would still accept the older no longer generated encodings as well as the newer encodings for backwards compatibility.  Third party applications that used $LIST encodings without using the corresponding $LIST functions would be broken until they were modified to support both old and new encodings.  However, when there were new $LIST encodings that were not used in all possible cases then the $LIST functions may not properly decode that encoding in cases that were never generated.  Third party software would find that some $LIST functions would not work if third party software made a choice to use a $LIST encoding that had never been generated by the InterSystems supplied $LIST functions.  Having ZZDUMP no longer document $LIST formats resulted in a great reduction in such incompatibilities.

Publishing a full specification of the $LIST encodings complete with usage restrictions would allow ObjectScript to support programs that manipulated these encodings without being limited to using only the $LIST functions.  It would also freeze the $LIST encodings on existing data types and would not allow the specification to be extended to provide future optimizations.  On the other hand, supporting customer debugging often requires the exchange of information involving these internal specifications so we often ask customers to look at these encodings.  Discussing the encoding specifications in a debugging context is useful.  However, depending on the internal details of these specifications not changing in the future is much less useful and that is discouraged.

Steven Hobbs · May 24, 2022 go to post

Actually,  the implementation of the $LIST functions has changed a lot in the past 25 years.  There have been 28 changes to the $LIST kernel code since it became part of IRIS and over 100 changes while it was part of Caché.  The changes were carefully done so that working programs that use functions of the form $LISTxxx, $Lx, and $LxS to manipulate $LIST strings will not notice the changes.  There are $LIST element encodings that are no longer generated but all the functions that examine a $LIST string will still correctly process the obsolete encodings.  New encodings were added to support new data types and to improve performance.

Code to READ compressed encodings for binary floating-point values and for Unicode strings have been available for over 5 years.  $LISTVALID in recent releases of IRIS and Caché would accept such compressed encodings even though they were not being generated.   Only the most recent releases have provided the ability to GENERATE these compressed encodings.  The $system.Process.ListFormat(n) classmethod will allow $LISTBUILD to generate these new compressed encodings.  Currently the new encodings  are turned off by default.  But any user who enables these $LIST optimizations will see new encodings in their $LIST strings.

Like the $ZHEX(n) function, the %DynamicAbstractObject classes that support for JSON representation can do some type conversions that were not inherited from the original ANSI M standard.  It might be slightly faster if you use an array instead of an object to save the conversion from a keyname into an array index.

    quit [(val)].%GetTypeOf(0)="string"

Steven Hobbs · May 23, 2022 go to post

One other quick comment:

Note that

WRITE ##class(DC.Test).IsString( )  //  -> 1

The IsString function takes a missing argument and assumes it is a string value.  Using an undefined variable as an argument to $LISTBUILD generates an "undefined" $LIST element and does not generate an <UNDEFINED> signal.

Steven Hobbs · May 23, 2022 go to post

I should mention that the numeric comparisons $A($LB(val),2)<3 and $A($LB(val,2)>3 are not quite legal ObjectScript since the internal specification of $LIST string representation is subject to extension in the future.  In previous Caché and IRIS releases the $LIST code type byte for a string could be 1 or 2.  In the future, $LISTBUILD will support compressing string values and those compressed $LIST string elements will use $LIST code type values greater than 3.

It is legal to use such undocumented $LIST representation techniques when debugging new code on a particular Caché/IRIS release.  However, depending on such internal representations in a production application should be avoided since the internal representations can change in the future.

Example: some programmers have used the string equality operation, ls1=ls2, to test if two $LIST strings contain the same values.  They should have used the $LISTSAME(ls1,ls2) function call.  This use of string equality on $LIST strings has resulted in broken applications in the past when extensions were added to the internal specification of $LIST strings.

Steven Hobbs · May 23, 2022 go to post

The original ANSI M (also known as ANSI MUMPS) standard did not distinguish between a canonical numeric string versus a number.  Many early implementations of this standard used strings of numeric characters as the internal representation of a number and did arithmetic on character strings instead of converting the numeric string to a binary representation and then using the hardware binary arithmetic instructions.  The result of a numeric operation would always be a character string using the canonical numeric representation.

In such an implementation there was no way to tell the difference between the character string "123" and the literal 123 since "123" was the canonical numeric string representation for that value.  For performance reasons, InterSystems ObjectScript has several different internal representations it can use for the number 123.  It can use the three character strintg "123"; it can use the 32-bit binary integer 123; it can use one of several representations of 123 in ObjectScripts' decimal floating point representation which has a 64-bit binary integer significand combined with an 8-bit power-of-10 exponent.  Writing 12300E-2 will usually be represented with the decimal floating-point representation and not with the integer repersentation.

The command WRITE $LISTSAME($LISTBUILD("123"),$LISTBUILD(12300E-2)) will write 1 because the string "123" and the numeric literal 12300E-2 are the same value.  However WRITE $LISTBUILD("123")=$LISTBUILD(12300E-2)) will write 0 because the $LISTBUILD will will generate different string encodings in this situation even though the values are considered to be identical.  Although $LISTBUILD could convert different internal representations into identical $LIST strings, it is faster just to place the internal representation into the generated binary string.  This is why you must use $LISTSAME when you want to check if two $LIST strings contain the same value.

There are a very few functions built into ObjectScript that have a behavior that depends on the internal representation of the argument.  All of these function were inherited when InterSystems extended ObjectScript to have a compatible feature with the language implemented by other vendors who produced extended M/MUMPS systems.  The most common function is $ZHEX(x) which turns a hex string to a decimal number and which turns a decimal number into a string.  Thus, WRITE $ZHEX("10") will write number 16 while WRITE $ZHEX(10) will write the string "A".  If you want to convert variable 'x' from an integer to hex then you need to write $ZHEX(+x) to make sure 'x' is using an internal numeric representation.  If you want to convert variable 'x' from hex to an integer then you need to write $ZHEX(x_"") to make sure 'x' is using the string internal representation.

One additional note:  The literal 123.0 will use an internal numeric representation so WRITE "123.0"=123.0 will WRITE 0 since a string equality comparison operator will be done between the literal string "123.0" and the canonical numeric string "123".  However, WRITE +"123.0"=123.0 will WRITE 1 because the unary plus operator converts the first operand to a numeric representation and then the string equality operator converts both numeric operands into their canonical numeric string representations making the string equality operation be the same as "123"="123".

Steven Hobbs · May 18, 2022 go to post

The %Library.DynamicAbstractObject, %Library.DynamicArray and %Library.DynamicObject classes with the %ToJSON and %FromJSON methods first appeared in Caché 2016.2.  I believe that 2016.2 also included JSON constructor syntax in ObjectScript.  I.e., you could write SET jsonvar={"number":1.2, "string":"abcd",null,true} as an executable ObjectScript statement.  Any JSON constant array or object is legal.  I am not sure when JSON constructors involving ObjectScript expressions was allowed.  E.g. SET jsonvar={"number":(1+.2+variable1), "string":("abcd"_variable2),null,false}.  Newer versions of Caché have more JSON support and IRIS has even more support, including the %JSON.Adapter class.  Inheriting the %JSON.Adapter class makes it possible to use the %JSONImport/%JSONExport methods which can read/writeJSON representation into/from ordinary class definitions.

Steven Hobbs · May 18, 2022 go to post

In particular,  an object is garbage collected when there are no ways to access that object using oref values starting from an oref-type value stored in a local variable.  When an oref is converted to an integer (or to a string) then that does not count as an object reference.  Store that integer/string in a PPG and then do KILLs (or otherwise make changes) of oref-type values referring to an object then that object will be deleted despite the fact a PPG (or any other variable) contains an integer/string reference to the object.

Steven Hobbs · Apr 21, 2022 go to post

Sorry about that.  My browser only showed this part of the URL:   https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?...  I did not notice ?... which indicated a longer URL than what I was really looking at.  The shorter URL I thought I was looking at gives the Class Reference page of the InterSystems documentation web site.

The SubclassOf query in ##class(%Dictionary.ClassDefinitionQuery) executed by a SQL SELECT command on a particular instance does provide a way to look at the subclasses of a class on that instance.  The query provides an even better way for executing code in program that needs to do a run-time search for information that is machine readable.

The local Class Reference web page supported by a particular Caché instance is probably an easier way to find a human readable display of the subclasses.  After clicking on a SubClasses Summary links then you probably want to again click on other links (especially the nested SubClasses Summary links.)
 

Steven Hobbs · Apr 20, 2022 go to post

The class reference at docs.intersystems.com will tell you about InterSystems supplied classes but it will not tell you about user written classes that are extended from a system supplied abstract classes nor about a user class extended from other user classes.

Look at the following with your browser:  http://localhost:57772/csp/documatic/%25CSP.Documatic.cls , where localhost is the system name on which you are running Caché and 57772 is the WebServer port of your Caché instance.  (If you were running IRIS your WebServer port would look something like 52773.)

Once you are on the Class Reference web page then navigate to find the particular class in which you are interested.  Look at the SubClasses Summary section to find inherited classes.  You can click on the an inherited class name to navigate to the Class Reference information of that class.

Steven Hobbs · Apr 19, 2022 go to post

Leon has not made a return comment and his original question would be ambiguous to some people.  I often call the " character, the 'double quote' character while I often call the ' character the 'single quote' character.  Other names I might use for the " character are 'quotation character' or 'ditto mark'.  Other names for the ' character would be 'apostrophe' or 'apostrophe quotation character'.  [[Notice I am using apostrophe quotation characters in this comment.]]  I might describe "" characters as 'doubled quotation characters' or 'doubled double quotes'.

Since Leon asked to 'remove double quotes (") from a string' my first impression would be to remove all " characters from the string.  I.e., every $C(32) should be removed.  If Leon wanted to only remove 'doubled quotation characters' then he probably would have written 'remove double quotes ("") from a string'.

Steven Hobbs · Apr 11, 2022 go to post

If your long strings are coming from JSON representation then

   Set DynObj=##class(%DynamicObject).%FromJSON(...)

will create a  %Library.DynamicObject or %Library.DynamicArray object in memory containing the JSON array/object elements where the sizes are limited only by the amount of virtual memory the platform will allocate to your process.  A string element of an object/array can have many gigabytes of characters (virtual memory permitting) and you can get the value of such a huge string element in the form of an in-memory, read-only %Stream doing:

   Set StreamVal=DynObj.%Get(key,,"stream")

in cases where DynObj.%Get(key) would get a <MAXSTRING>.

The StreamVal (class %Stream.DynamicBinary or %Stream.DynamicCharacter) is a read-only, random-access %Stream and it shares the same buffer space as the 'key' element of the DynObj (class %Library.DynamicObject) so the in-memory %Stream does not need additional virtual memory.

You can then create a persistent object from a class in the %Steam package (%Stream.GlobalBinary, %Stream.GlobalCharacter, %Stream.FileBinary, %Stream.FileCharacter, or some other appropriate class.)  You can then use the CopyFrom method to populate the persistent %Stream from the  read-only, in-memory %Stream.DynamicBinary/Character.

Steven Hobbs · Apr 4, 2022 go to post

Consider the following:

USER>set num1=1,str1="""1""",num2=2,str2="""2""",num10=10,str10="""10""",abc="abc"

USER>set (x(num1),x(str1),x(num2),x(str2),x(num10),x(str10),x(abc))=""            

USER>set i="" do { set i=$order(x(i))  q:i=""   write i,! } while 1               
1
2
10
"1"
"10"
"2"
abc

The WRITE command treats its expression arguments as ObjectScript string expressions so any argument expression with a numeric value is converted to a string value containing characters.  Note that variables str1 and str2 are strings containing 3 characters starting with one double-quote character, ".  The str1 and str2 values sort among other strings where the first character is a double-quote character.  When you print these subscript strings, the subscripts from variables num1 and num2 print as one character strings,  the subscript from variable num10 prints as a two character string, the subscripts from variables sub1, sub2 and abc print as three character strings and the subscript from variable str10 prints as a 4 character string.

If you use ZWRITE instead of WRITE then ZWRITE will add extra quote marks, will add $C(int) expression syntax and will add other things so that the textual representation that is printed does not contain any unprintable characters and you can see trailing white space.

Steven Hobbs · Apr 4, 2022 go to post

I wasn't advocating changing a system collation or changing a namespace collation.  I only recommended using ##class(%Library.GlobalEdit) to change the subscript collation of a newly created individual global variable, leaving all other globals unchanged.

You can see the collations loaded into an instance by executing 'DO  ^|"%SYS"|COLLATE' .  I believe in this case the user wants to use built-in collation 133, which should be the version of collation 5 that sorts only strings and does not sort numbers.  It looks like 133 is now considered to be a "legacy collation" as I have problems finding it in the on-line documentation.

Steven Hobbs · Apr 4, 2022 go to post

You might consider looking at the ##class(%GlobalEdit).Create(...) method.  It has a 'Collation' argument which allows you to change the collation from the namespace default.  Just choose a collation that does *NOT* sort canonical numeric strings in front of non-numeric strings.

I don't recommend ever changing the default collation of a namespace as many utility routines depend on canonical numeric strings sorting in numeric order and not sorting in string order.  Those utilities may not work in such a namespace.

Steven Hobbs · Jan 28, 2022 go to post

The %GlobalCharacterStream class is deprecated (although I believe it will continue working.)  You should consider replacing it with the %Stream.GlobalCharacter class.
 

Steven Hobbs · Jan 4, 2022 go to post

You might consider using the %Library.DynamicObject and %Library.DynamicArray classes which are built into ObjectScript.  ObjectScript supports JSON constructors for %DynamicObject, {...}, and for %DynamicArray, [...].  a %DynamicObject/Array can contain both JSON values and ObjectScript values.   There is also a %FromJSON(x) which reads JSON objects/arrays when x is a %Stream or a FileNme or an ObjectScript %String containing JSON syntax.  Here is an example from a recent IRIS release:


USER>zload foo

USER>zprint
foo()    {
            set DynObj = {
            "notanumber":"28001",
            "aboolean":true,
            "anumber":12345,
            "adecimal":1.2,
            "adate":"01/01/2023",
            "adate2":"01-01-2023",
            "anull":null,
            "anull2":null,
            "anull3":null
            }
            write DynObj.%ToJSON(),!,!
            set DynObj.Bool1 = 1    ; Set without type
            do DynObj.%Set("Bool2",1,"boolean") ; Set with type
            set DynObj.old1 = "abc"   ; Set without type
            do DynObj.%Set("new1","abc","null") ; Set with type
            set DynObj.old2 = ""   ; Set without type
            do DynObj.%Set("new2","","null") ; Set with type
            write DynObj.%ToJSON()
}        

USER>do ^foo()
{"notanumber":"28001","aboolean":true,"anumber":12345,"adecimal":1.2,"adate":"01/01/2023","adate2":"01-01-2023","anull":null,"anull2":null,"anull3":null}

{"notanumber":"28001","aboolean":true,"anumber":12345,"adecimal":1.2,"adate":"01/01/2023","adate2":"01-01-2023","anull":null,"anull2":null,"anull3":null,"Bool1":1,"Bool2":true,"old1":"abc","new1":"abc","old2":"","new2":null}

Your can use ordinary ObjectScript assignment to assign ObjectScript values to entries in a %DynamicObject/Array.  You can use the %Set(key,value,type) method to assign ObjectScript values into an object with an optional type parameter controlling which JSON value to which to convert the ObjectScript value.  I.e., type "boolean" converts ObjectScript numeric expressions into JSON false/true values and type "null" converts an ObjectScript %String into a JSON string *except* that the empty string is converted to the JSON null value.  (Note:  some older implementations of %Set with type "null" only accepted "" as the value while new implementations accept any string expression as the value.)

The JSON constructor operators in ObjectScript, besides accepting JSON literals as values, can also accept an ObjectScript expression enclosed in round parentheses. 

E.g.,  SET DynObj2 = { "Pi":3.14159, "2Pi":(2*$ZPI) }

Steven Hobbs · Dec 8, 2021 go to post

When asking about a 'ROUTINE' you may be asking about the difference between a 'Routine', 'Procedure', 'Subroutine', 'Function', 'Label', 'Class' and 'Method'

A 'Routine' is a source code file with name like 'myroutine.mac'.  Source code can also be a 'Method' which is found in a 'Class' file with a name like 'myclass.cls"

A Routine file can contain Procedures, Subroutines, Functions and Labels.  See https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCOS_usercode#GCOS_usercode_overview for some InterSystems documentation.

You can call a subroutine with the statement 'DO MySubroutine^myroutine(arg)'; You can call a function with the expression '$$MyFunction^myroutine(arg)';  You can call the procedure 'MyProcedure^myroutine(arg)' using either the syntax for a subroutine call or function call depending on whether you need the value returned by the procedure; and, You can Goto the source code following a label with the statement 'GO MyLabel^myroutine'.  If you reference a subroutine/function/procedure/label inside a Routine file (e.g. myroutine.mac) and that subroutine/function/procedure/label access (e.g.$$MyFunction^myroutine(arg)) is referencing a name defined in the same Routine file then you do not have to specify the ^Routine name in the call syntax (e.g. $$MyFunction(arg)).

The local variables used inside a subroutine or function are 'public' variables and those named variables are shared with all other subroutines and functions.  You can use the NEW command inside a subroutine or function to temporarily create new public variables without modifying previous instances of public variables with the same names.

The local variables used inside a procedure are private variables.  The private variables are only available inside the procedure.  Those names do not conflict with variables used by any caller of the procedure.  Those names are not available in any code called by the procedure although it is possible to pass either the private variable or the value of the private variable as an argument when calling out of a procedure.  The declaration of a procedure can optionally include a list of public variables.  Names in public list reference the public variable when they are accessed by code in the procedure.  You can use the NEW command with an argument that is a public variable within a procedure.  A label defined inside a procedure is also private and it cannot be referenced by any GO command outside that procedure.

Methods defined in a class file default to being a procedure with private variables and private labels.  However, it is possible to specify that a method is a subroutine or function.   Also, a method procedure declaration can optionally include a list of global variables.

Steven Hobbs · Dec 1, 2021 go to post

The %GlobalBinaryStream class is deprecated and newly written code should instead use %Stream.GlobalBinary.  Of course, existing code that is not needing modification can continue using %Library.GlobalBinaryStream.
.  

Steven Hobbs · Nov 7, 2021 go to post

“AlertText” is not the name of a JSON object field but is instead the value of a JSON array element.  Assuming you have done your %FromJSON into variable ‘data’ then data.%Get(“params”).%Get(0) will evaluate to the string value “AlertText”.

Steven Hobbs · Oct 27, 2021 go to post

As mentioned in another Developer Community Question, a recent version of IRIS would allow you to  evaluate object.%Get("pdfKeyName",,"stream") which would return to you an %Stream object containing the JSON string in question as raw characters.  Also, %Get in IRIS can support object.%Get("pdfKeyName",,"stream<base64") which would do Base64 decoding as it creates the %Stream. However, you said you need to stick with an older version of Caché/Ensemble which predates these %Get features.

It is possible to convert the long JSON string component to a %Stream but it will take some parsing passes.

(1) First use SET json1Obj=[ ].%FromJSON(FileOrStream) to create json1OBJ containing all the elements of the original JSON coming from a File or Stream.

(2) If your pdf JSON string is nested in json1OBJ then select down to the closest containing %DynamicObject containing your pdf JSON string,  I.e. Set json2Obj=json1Obj.level1.level2 if your original JSON looks like {"level1":{"level2":{"pdfKeyName":"Very long JSON string containing pdf", ...}, ...}, ...}

(3) Create a new %Stream containing the JSON representation of that closest containing %DynamicObject.  I.e.,

   SET TempStream=##class(%Stream.TmpBinary).%New()
   DO json2Obj.%ToJSON(TempStream)

(4) Read out buffers from TempStream looking for "pdfKeyName":" .  Note that this 14-character string could span a buffer boundary.

(5) Continue reading additional buffers until you find a "-characer not preceded by a \-character; Or until you find a "-character preceded by an even number of \-characters.

(6) Take characters read in step (5) and pass them through $ZCVT(chars,"I","JSON",handle) to convert JSON string escape characters to binary characters and then write them to a %Stream.FileBinary (or some other Binary %Stream of your choosing.)

(7) If your JSON "pdfKeyName" element contains Base64 encoding  then you will also need to use $SYSTEM.Encryption.Base64Decode(string). Unfortunately $SYSTEM.Encryption.Base64Decode(string), unlike $ZCVT(chars,"I",trantable,handle), does not include a 'handle' argument to support the buffer boundary cases in those situations where you have broken a very large string into smaller buffers.  Therefore you should remove the whitespace characters from 'string' before calling $SYSTEM.Encryption.Base64Decode(string) and you must make sure the argument 'string' has a length which is a multiple of 4 so that a group of 4 data characters cannot cross a buffer boundary.

Again the more complete support in IRIS can do steps (2) through (7) without worrying about buffer boundaries by executing

   Set BinaryStreamTmp = json1Obj.level1.level2.%Get("pdfKeyName",,"stream<base64")

and you can then copy the BinaryStreamTmp contents to wherever you want to resulting .pdf file.

Steven Hobbs · Oct 26, 2021 go to post

The %DynamicAbstractObject classes first appeared in version 2016.2 so JSON support in software versions 2017.x will not have many of the more recent new features.  If possible, you really should upgrade to more recent InterSystems versions so you can use more modern functionality.

Steven Hobbs · Oct 26, 2021 go to post

The latest version of IRIS will have improved %DynamicObject (and the %DynamicArray) class objects.  They will support a method call like obj.%Get(key,,”stream”) which will return a %Stream.DynamicCharacter oref and this %Stream can contain a very large number of characters.  This can be copied into a %Stream.GlobalCharacter or a %Stream.FileCharacter if you want to save those characters in a persistent object.

This new form of %Get will also be able to include encoding/decoding using Base64 representation.  Similar extensions have been added to the %Set method.

Steven Hobbs · Oct 25, 2021 go to post

You should not follow the recommendation to modify the $ZTIMEZONE system variable.  See this warning in the $ZTIMEZONE documentation:

Note:
Changing the $ZTIMEZONE special variable is a feature designed for some special situations. Changing $ZTIMEZONE is not a consistent way to change the time zone that Caché uses for local date/time operations. The $ZTIMEZONE special variable should not be changed except by those programs that are prepared to handle all the inconsistencies that result.

On some platforms there may be a better way to change time zones than changing the $ZTIMEZONE special variable. If the platform has a process-specific time zone setting (for example, the TZ environment variable on POSIX systems) then making an external system call to change the process-specific time zone may work better than changing $ZTIMEZONE. Changing the process-specific time zone at the operating system level will change both the local time offset from UTC and apply the corresponding algorithm that determines when local time variants are applied. This is especially important if the default system time zone is in the Northern Hemisphere, while the desired process time zone is in the Southern Hemisphere. Changing $ZTIMEZONE changes the local time to a new time zone offset from UTC, but the algorithm that determines when local time variants are applied remains unchanged

If you change $ZTIMEZONE then the local time will change but local changes in timezone rules (e.g., entering/leaving Daylight Saving Time, DST) will not be changed.  If the system is in the Northern Hemisphere but you want a local time in the Southern Hemisphere (or vice-versa) then DST changes will be backwards.  Also, DST rules near the equator can be very different from the DST rules at latitudes closer to the poles.  If the system local timezone and the modified local timezone are in different countries then the national date/time rules may be incorrect for the modified local timezone.

Using the $SYSTEM.Process.TimeZone(...) method suggested by Jon Willeke is the best way to modify the local timezone used by a Caché/IRIS process.  However, the 'TZ' environment variable modified by the $SYSTEM.Process.TimeZone(...) method requires an argument string that is specific to the Operating System under which Caché/IRIS is running.  Generally the Windows Operating System wants the TZ variable to contain a POSIX format timezone string while Unix/Linux systems want the TZ variable to contain an Olson format timezone string (sometimes called the IANA or ICU format timezone string.)  If you need dates/times using rules from the past then generally the Olson format will work much better than the POSIX format.

Steven Hobbs · Sep 15, 2021 go to post

Aside from several arithmetic differences, the biggest difference between $INCREMENT AND $SEQUENCE occurs when "SET index=$INCREMENT(^global)" versus "SET index=$SEQUENCE(^global)" are being executed by multiple processes.

All the processes evaluating $INCREMENT(^global) on the same ^global variable will see a sequence of increasing integers.  No two processes will see the same integer returned.  The integers are given out in strict increasing time order and no integer value is skipped.

All the processes evaluating $SEQUENCE(^global) on the same ^global variable will see a sequence of increasing integers.  No two processes will see the same integer returned.  Because blocks of integers are assigned to processes, it is possible for one process to receive a larger integer in the sequence before some other process receives a smaller integer in the sequence.  If some process decides to stop processing integer values at some point then the larger integer values assigned to that process will be skipped and not returned as part of the sequence.

The $SEQUENCE function can have less multi-process overhead because integers in the sequence are assigned in blocks but $SEQUENCE does not guarantee a sequence of numbered tasks is assigned to processes in strictly increasing order.  Every process must continue processing numbered tasks until that particular process has been assigned a sequence number larger than the highest assigned task.  Just because one process has finished the highest assigned task does not mean that other processes are done with the earlier tasks (or have even started earlier tasks) and the sequence is only complete when every process has received a sequence number larger that the number of the final task.

Steven Hobbs · Jul 22, 2021 go to post

The JSON classes in Ens.Util.JSON,  %ZEN.Auxiliary.json* and %JSON.* all contain methods that convert JSON representation to/from ObjectScript classes.  Once you have an ordinary ObjectScript Class then you are using ObjectScript data types for Property values.  The JSON null is usually converted to "" (null string).  Also, ordinary Property variables of an ObjectScript Class are never undefined but are automatically initialized to "" (the null string).  [[ An exception is [MULTIDIMENSIONAL] Properties which can be undefined but by default such Property variables do not participate in %Save() and JSON/XML export/import operations. ]]  SQL operations involving Class properties treat "" (the null string) as the SQL NULL value and SQL assumes a Class Property containing the ObjectScript string $C(0) is the empty string.

[[ Although the original question involved Caché and not IRIS, IRIS has signifcantly more complete support for the %DynamicAbstractObject classes so my examples will use IRIS.  If possible, I recommend upgrading to IRIS. ]]

There is the class %Library.DynamicAbstractObject and its subclasses %DynamicArray and %DynamicObject that can contain elements which can either be JSON values or ObjectScript values.  The ObjectScript statement:

USER>SET x={"a":null,"b":"","e":1.0,"f":"1.0","g":(00.1),"h":($c(0))}

makes x be a %DynamicObject oref where element a is a JSON null and where element "g" is the ObjectScript number .1 and element "h" is the ObjectScript string $c(0).  Note that if an ObjectScript expression is a %DynamicObject constructor then ObjectScript parses the elements of that constructor using JSON syntax except for the extension to constructor syntax where an element inside round parentheses is parsed as an ObjectScript run-time expression generating an ObjectScript value.

You can convert a %DynamicObject to JSON string representation and any ObjectScript valued element will be converted to JSON representation.

[[ Note that JSON does not support certain ObjectScript values: $double("NAN"), $double("INFINITY") and orefs that are not a subclass of %DynamicAbstractObject.  A %DynamicAbstractObject containing such an ObjectScript element cannot be converted to JSON. ]]

USER>WRITE x.%ToJSON()
{"a":null,"b":"","e":1.0,"f":"1.0","g":0.1,"h":"\u0000"}

Evaluating a %DynamicObject element in an ObjectScript expression converts that element to an ObjectScript value.

USER>ZWRITE x.a, x.b, x.c, x.e, x.f, x.g, x.h
""
""
""
1
"1.0"
.1
$c(0)

Notice that the undefined element x.c is converted to the ObjectScript null string.  You never get an error evaluating x.%Get(key) for any value of the expression key as every undefined element in a %DynamicObject has the value of the null string.  Also, x.a, which contains a JSON null, is converted to the ObjectScript null string.  The JSON treatment of undefined elements and the ObjectScript treatment of undefined Properties means that when we convert an ordinary ObjectScript class to either XML or JSON then we can skip converting a Property with the null string value as converting JSON or XML back to an ordinary class object will result in all unrepresented properties getting the value of the null string.

If you need to know if a %DynamicObject element is JSON null, null string or undefined then evaluating the %GetTypeOf(key) will tell you that.

USER>ZWRITE x.%GetTypeOf("a"),x.%GetTypeOf("b"),x.%GetTypeOf("c"),x.%GetTypeOf("e")
"null"
"string"
"unassigned"
"number"

The %FromJSON(stream)/%ToJSON(stream) methods will let you read/write JSON representation from/to a %Stream.

[[ Things that only work in IRIS follows. ]]

The size of the %DynamicArray/%DynamicObject class objects is limited only by the amount of memory available to your process.  A string valued %DynamicObject element can be significantly longer than the maximum length supported by ObjectScript string values.  If you have such a long string element then you will have convert that element to an ObjectScript %Stream in order to manipulate it in ObjectScript.

USER>SET stream=x.%Get(key,,"stream")  ;; Note 3 arguments with 2nd argument missing

will generate an in-memory, readonly %Stream that can be copied to a Global or File %Stream or can be examined by reading that string in small pieces.

In recent IRIS releases you can do

USER>SET binarystream=x.%Get(key,,"stream<base64")

which will convert a base64 encoded element into a readonly binary %Stream.  You can do the reverse conversion by evaluating x.%Set(key,binarystream,"stream>base64").  See the the Class Reference documentation pages for the %Library.%DyanmicAbstractObject class and its %DynamicArray and %DynamicObject subclasses for more details.

Steven Hobbs · Jun 23, 2021 go to post

By the looks of it, the original question applied $DATA to an element of the %DynamicAbstractObject classes.  The $DATA and $GET functions can only be applied to Multidimensional variables.  All ObjectScript local and global variables are Multidimensional.  By default a property variable of a class object is not multidimensional unless the property default is over ridden with the [ Multidimensional ] attribute.  The %DynamicAbstractObject classes provide no way of supporting elements which are Multidimensional.  If dynobj is an object reference to a %DynamicObject then dynobj.%Get(key) is defined for all possible string values of 'key'.  If dynobj.%GetTypeOf(key) returns "unassigned", or "null", then dynobj.%Get(key) will return the empty string value.

Currently, $DATA(dynobj.keyname) signals <PROPERTY DOES NOT EXIST> which is what is signaled for by all classes if the Property 'keyname' does not exist.  In a future IRIS release it will report

<INVALID CLASS> *Class '%Library.DynamicObject' does not support MultiDimensional operations

The correct way to see if keyname "test" exists in a %DynamicObject is to evaluate (dynobj.%GetTypeOf("test") '= "undefined") .

Steven Hobbs · Jun 2, 2021 go to post

The %DynamicAbstractObject classes (%DynamicArray and %DynamicObject) are not %Persistent as they cannot be directly written into a database of class objects.  However, most %DynamicAbstractObject objects can be serialized with the %GetSerial method which produces a JSON string value or with the %GetSwizzleObject method which builds an OID containing the JSON string value.  And a Serial object can be the value of a Property internal to a %Persistent object.

The %DynamicAbstractObject objects can contain elements which are either JSON values or ObjectScript values. Any element which can be translated into a JSON value can be serialized with the %GetSerial/%GetSwizzleObject methods.  However, if a %Dynamic object contains ObjectScript values that are not representable in JSON then that %Dynamic object cannot be serialialized.  Examples of ObjectScript values that cannot be translated into JSON include $double("nan"), $double("infinity"), $double("-infinity") and any oref not a member of the %DynamicArray or %DynamicObject classes.  Also, the %DynamicArray and %DynamicObject classes can contain string element values much longer than the %String values supported by ObjectScript.  If the JSON translation of a %DynamicArray or a %DynamicObject results in a string longer than the ObjectScript maximum string length then the serialization of the object will fail.  However, the JSON translation of a large %Dynamic object can be stored into a class of the %Stream package that is abled to be swizzled.  Such a swizzled object can be a Property in a %Persistent object.  However, the %Stream property must be processed by the %FromJSON(stream) method in order to turn the %Stream into a %DynamicAbstractObject object.

Steven Hobbs · May 3, 2021 go to post

Since the JSON content of 'members' property is a JSON array (aka the ObjectScript class %DynamicArray) and that array contains a list of JSON objects (aka the ObjectScript class %DynamicObject), I assume you don't really want a members array containing less that 1 element to be the null JSON value but instead you want that members value to be an empty array, I.e.

    set ObjectScriptObject.members=[ ]

Making this choice would mean ObjectScriptObject.members is always a %Dynamic.Array containing 0 or more elements.

A *property* of a class always contains an ObjectScript value that can be a string, a canonical decimal number, a $DOUBLE(...) IEEE 64-bit floating-point value or an oref.  An oref is a reference to a Class type object which includes the %DynamicArray and %DynamicObject classes.  The %DynamicArray/Object classes are special in that they can contain zero or more elements where each element can either be a JSON value or an ObjectScript value.  Where appropriate, conversions will be performed between JSON values and ObjectScript values.

ObjectScript variables and [MULTIDIMENSIONAL] properties can contain ObjectScript arrays of ObjectScript values.  These ObjectScript arrays are hierarchical, or tree like.  Internal and leaf elements can have a value or can be undefined.  A non-multidimensional property and an ObjectScript valued element in a %DynamicArray/Object object are not hierarchical and not undefined.  Non-multidimensional properties and elements that have not been initialized will have the empty string, "", as their value the first time they are accessed.

Steven Hobbs · Apr 28, 2021 go to post

You are using a 5 year old version of Ensemble that is using an experimental release of JSON support with classes named %Library.AbstractObject, %Library.Object and %Library.Array.  When you upgrade to any version after 2016.1 you will find the JSON support classes are now named %Library.DynamicAbstractObject, %Library.DynamicObject and %Library.DynamicArray.  All of the method names will change because support for system methods with names that start with $ has been removed.  Method names like  $getIterator, $fromJSON and $getNext will become %GetIterator, %FromJSON and %GetNext.  Other than the extensive name changes, most of features of the experimental JSON classes has remained unchanged.

There are additional new features added during future releases. For example, the ObjectScript language will accept JSON object and array constructor syntax as part of the ObjectScript expression syntax.  If an element which is part of an object/array constructor inside an ObectScript program is enclosed in round parentheses then that element is evaluated using ObjectScript expression syntax instead using JSON literal syntax.  There will also be some additional ways to control the conversions between JSON types and ObjectScript types.