Get object by propertyPathString when dot.notation syntax can not used

In normal use, it is usually strategically better to use only the hard-coded dot notation syntax. This should also be adhered to as far as possible and sensible and there are no application-specific special features that do not allow a fixedly coded dot notation syntax without taking too long detours.

$obj2.name:=$obj.names.firstName

Special use cases

Sometimes, however, depending on the situation,
only any(individual unknown dynamic) propertyPath is available
as a string and you must access the object via this specification
without knowing before which names can contains the individual propertyPath string.

Given feature:

OB Get ( object ; property {; type} ) // OK
$obj.names["firstName"] // OK

A missing feature:

OB Get ( object ; propertyPath {; type} ) // Error
$obj["names.firstName"] // Error

Workaround

Workaround method for missing “OB Get(object; propertyPath{; type})”
Project method “zObjGetByPropertyPath(object; propertyPath{; type})”

  // PM: "zObjGetByPropertyPath"
  // zObjGetByPropertyPath(object; propertyPath{; type}) // -> Function result

C_VARIANT($result;$0)
C_OBJECT($objSrc;$1)
C_TEXT($propertyPath;$2)
C_LONGINT($type;$3)

C_COLLECTION($colKeys)
C_LONGINT($i)
C_OBJECT($obj)

If (Count parameters>0)
  $objSrc:=$1
  If (Count parameters>1)
    $propertyPath:=$2
    If (Count parameters>2)
      $type:=$3
    End if 
  End if 
End if 

$colKeys:=Split string($propertyPath;".")

If (Count parameters<3)  // destination type for result is NOT defined, than return original type
  Case of 
    : ($colKeys.length<1)
      $result:=OB Get(New object;"xy")
      
    : ($colKeys.length<2)
      $result:=OB Get($objSrc;$colKeys[0])
      
    : ($colKeys.length<3)
      $result:=OB Get(OB Get($objSrc;$colKeys[0];Is object);$colKeys[1])
      
    Else 
      $obj:=OB Get($objSrc;$colKeys[0];Is object)
      For ($i;1;$colKeys.length-2)
        $obj:=OB Get($obj;$colKeys[$i];Is object)
      End for 
      
      $result:=OB Get($obj;$colKeys[$colKeys.length-1])
  End case 
  
Else   // destination type for result is defined, than try convert original-type to destination-type
  Case of 
    : ($colKeys.length<1)
      $result:=OB Get(New object;"xy";$type)
      
    : ($colKeys.length<2)
      $result:=OB Get($objSrc;$colKeys[0];$type)
      
    : ($colKeys.length<3)
      $result:=OB Get(OB Get($objSrc;$colKeys[0];Is object);$colKeys[1];$type)
      
    Else 
      $obj:=OB Get($objSrc;$colKeys[0];Is object)
      For ($i;1;$colKeys.length-2)
        $obj:=OB Get($obj;$colKeys[$i];Is object)
      End for 
      
      $result:=OB Get($obj;$colKeys[$colKeys.length-1];$type)
  End case 
  
End if 

$0:=$result

  // - EOF -

Examples for using project method “zObjGetByPropertyPath”

C_OBJECT($obj)
$obj:=New object("b";New object("b2";New object("b3";9)))

$result1:=yObjGetByPropertyPath ($obj;"b.b2.b3")  // -> 9 (when type not defined, than automatic result-type is orginal-type)
$result2:=yObjGetByPropertyPath ($obj;"b.b2.b3";Is longint)  // -> 9
$result3:=yObjGetByPropertyPath ($obj;"b.b2.b3";Is real)  // -> 9
$result4:=yObjGetByPropertyPath ($obj;"b.b2.b3";Is variant)  // -> 9
$result5:=yObjGetByPropertyPath ($obj;"b.b2.b3";Is text)  // -> "9"
$result6:=yObjGetByPropertyPath ($obj;"b.b2.b3";Is date)  // -> !00-00-00!
$result7:=yObjGetByPropertyPath ($obj;"b.b2.b3";Is time)  // -> ?00:00:09?
$result8:=yObjGetByPropertyPath ($obj;"b.b2.b3";Is boolean)  // -> True
$result9:=yObjGetByPropertyPath ($obj;"b.b2.b3";Is object)  // -> null
$result10:=yObjGetByPropertyPath ($obj;"b.b2.b3";Is collection)  // -> null
$result11:=yObjGetByPropertyPath ($obj;"b.b2.b3";Is pointer)  // -> Nil

$result20:=yObjGetByPropertyPath ($obj;"b.b2")  // -> {"b3":9} (when type not defined, than automatic result-type is orginal-type)
$result21:=yObjGetByPropertyPath ($obj;"b.b2";Is object)  // -> {"b3":9}
$result22:=yObjGetByPropertyPath ($obj;"b.b2";Is text)  // -> "[object Object]"
$result23:=yObjGetByPropertyPath ($obj;"b.b2";Is collection)  // -> null


C_OBJECT($objSub;$objSubSub)

$objSubSub:=New object
$objSubSub.valA:=91
$objSubSub.valB:=92  // **

$objSub:=New object
$objSub.aa:=1
$objSub.bb:=2
$objSub.cc:=$objSubSub

$obj:=New object
$obj.id:="testX"
$obj.name:="test"
$obj.obj:=$objSub

$result:=yObjGetByPropertyPath($obj;"obj.cc.valB";Is longint)  // -> 92

Commands/Functions can use only property (exact max one key name):

  • OB Get ( object ; property {; type} ) // -> Function result
  • OB GET ARRAY ( object ; property ; array )
  • OB SET ( object ; property ; value {; property2 ; value2 ; … ; propertyN ; valueN} )
  • OB SET ARRAY ( object ; property ; array )

Commands/Functions can use a propertyPath:

  • collection.extract ( propertyPath {; targetPath}{; propertyPath2 ; targetPath2 ; … ; propertyPathN ; targetPathN}{; option}) // -> Function result
  • collection.orderBy ( {criteria} ) // -> Function result
  • collection.distinct ( {propertyPath}{;}{option} ) // -> Function result
  • collection.sum ( {propertyPath} ) // -> Function result
  • collection.max ( {propertyPath} ) // -> Function result
  • collection.min ( {propertyPath} ) // -> Function result
  • collection.average ( {propertyPath} ) // -> Function result
  • collection.count ( {propertyPath} ) // -> Function result
  • collection.countValues ( value {; propertyPath} ) // -> Function result
  • collection.indices ( queryString {; value}{; value2 ; … ; valueN} ) // -> Function result
  • collection.query ( queryString {; value}{; value2 ; … ; valueN}{; querySettings}) // -> Function result
  • entitySelection.extract ( propertyPath {; targetPath}{; propertyPath2 ; targetPath2 ; … ; propertyPathN ; targetPathN}{; option}) // -> Result
  • entitySelection.orderBy ( criteria ) // -> Result
  • entitySelection.distinct ( attributePath {; option} ) // -> Result
  • entitySelection.toCollection ( {filter ;}{ options {; begin {; howMany}}} ) // -> Result
  • entitySelection.sum ( attributePath ) // -> Result
  • entitySelection.average ( attributePath ) // -> Result
  • entitySelection.max ( attributePath ) // -> Result
  • entitySelection.min ( attributePath ) // -> Result
  • entity.toObject ( filter {; options} ) // -> Result
  • dataClass.query ( queryString | formula {; value}{; value2 ; … ; valueN}{; querySettings}) // -> Result
  • Average ( series {; attributePath} ) // -> Function result
  • Sum ( series {; attributePath} ) // -> Function result
  • Min ( series {; attributePath} ) // -> Function result
  • Max ( series {; attributePath} ) // -> Function result

you are missing an important point; the dot character is allowed as property names (same in JavaScript).

$obj["names.firstName"] is not a path, it is a property whose name is “names.firstName”.

Dot-notation is a shorthand that can only be used with property names that work within its syntax constraints. Not all properties are accessible via dot, which is one of the reasons why we need the bracket syntax.

No i never forget this important point.
Yes a key-name can contain a dot and other special-chars,
but this is not a good naming one.

Yes, it is the reason why [“a.b”] …and maybe OB Get(obj;“a.b”)
in 4D (and in JavaScript to [“a.b”]) is a part of the key-name
to have for thuch exotic named keys a possibility to work with them
because a.b as hard coded dot syntax means always subObject b obj[“a”][“b”].
When you use/mustUse a key-name with exotic point in name,
take care, maybe you got a lot of problems (i never test it with for example JSONparse)
I totally understand why it is done this way,
and because there is nowhere a solution
to have getObjByPath …so i create for this needfool use case my own method
(with the same restrictions like 4D did it when using propertyPath/attributPath so many times)
and give it to all to use it when they need it sometimes.

Maybe my post here is missunderstood,
i wrote “missing” by OBget and [“x.y”]
to explain this is not possible
and this is the reason why i create a helper-function to can do this.

I wrote a big list of 4D examples where you can define a propertyPath by a string.
See in my last post “Commands/Functions can use a propertyPath:”
All these 4D examples works wrong when a key-name is given with a point in name,
because in this propertyPath-String the point is always interpreted as
a dot sepated object syntax (dot-notation).
And my new helper-method brings same feature for a object
because 4D built only for collection.extract(propertyPath)
and not for object.extract(propertyPath) …not realy extract, better called to zObjGetByPath

https://doc.4d.com/4Dv18/4D/18/Using-object-notation.300-4505639.en.html#Paragraph_3305214

Warning
Although object property names can contain special characters such as “.” or “[” or “]” (and are available through the $o[“My Att.name”] syntax), they are not recommended since you will not be able to perform queries or sorts on them. All 4D commands and methods that execute queries on object properties, such as dataClass.query( ) or QUERY BY ATTRIBUTE use a string as propertyPath or attributePath parameter, for example:

Thank you for the detailed explanation, I understand now.

Since native object notation is much faster than OB Get, allows mixing of bracket notation (prop[0].prop), what would be a practical use case for this helper-function? I am not being cynical, I am pretty sure there must be have been a case that prompted you to write this code, but I can not think of any.

First, thanks for reply and thanks for your explainations.

Yes hardcoded syntax of dot notation is faster
and like i wrote before (in normal use cases always the best choice)

But there are too exists thousands/millions of special use cases where
only a propertyPath-String is the only base to adress a object
and i can not list all (sorry, i am not a dictonary of all existing use cases in real world :wink:
and too i need a lot of time to remember all known use cases which i meet in the last 40years of programming.

Only one case, when a path/adress to a object is stored (in a field or anywhere),
then you can do this not with a pointer :wink:
you stored the adress as a String “a.b.c.item”
(or as a array or collection [“a”;“b”;“c”;“item”] in correct sort-order, this comes later, now only String)
to later fetch with this stored adress.

Or i need it, because i do not want to write Formula from string( “a.b.c.item”),
to dynamically adress a object with a given string-adress.

In generic/dynamic programming there are much situations where a fetch with string-adress is needed.
I want always keep my use-methods clean
(example, createCustomer contains only codes for this job and not lines for any base-tech-job)
so i write a lot of capsulated helper-methods (routines) for base-tech-jobs.

At the moment i begin to write my own query-editor,
same like 4D-query-editor
but with new missing features
to query inside object-fields (query by attribute and with attributPath).
My methods search in all records to get all existing keys an keyPaths.
A object-field has not always same contents and same existing/nonExisting keys/keyPaths.
The query-editor fully easy to use for anybody (same like the 4Ds query editor).
In addition you can edit your own sql-query-text (but this is only for pro-users).
For normal users there are popups to choose a field or a obj/subObj like field.a.b.c.item.
This popups contains all existing keys in all records to a table/field.
Sometimes you got from a rest-api any json-data,
and sometimes the api has not a fixed object-structure,
the structure is sometimes different in records,
productA has o.a.b.img and productB has o.a.b.c.img
or productA has o.a.price.discount and in productB there did no discount object exist.
I did not write all apis in the world :wink:
This apis are what they are and my job is it to can work with this structure of data.
This only a primitive example for how object structures can differs,
it is not a fixed structure of sql-table/fields
it is a free object structure what can have from record to record some little differences in structure.

Sorry, i can not give you a deep dive into a very complex solution-structure
of generic/dynamic programming,
to show the deeply buried subtleties,
why and why then in some places there for different reasons
Ultimately, only one object must be accessed via a string.

In my programming i need for thuch things
not a rational justification (simply explained in three words).
Like a professional gardener who knows all what his plants need,
but not can tell all of this in just some simple words.
My space is not limited to what i can articulate fine/clean/simple,
my space a totally out of this, i know what i know …no matter whether I can describe it clearly or not.
I always going the needed/correct/best way
and i do not limit/choose only ways which can easy describe/shown to others.
But with a little bit of phantasy (you know a lot of programming and so it is possible)
every pro-programmer knows for which thousand use cases propertyString is needed.

In addition to my last post,
only one (maybe/canbe) example for stored object pathes.
A user individual customized listbox
with a optional totally flat line/column structure.
The user can choose what columns he want in his listbox-variantA or B…,
he can choose for example all fields of a table or user-customized entity
and too he can choose for his own defined object field (with user individual object structure)
any of the attributes inside the object.
Any (of his own indivdual) attribute the user choose (example: myField.a.b.c.d.e.info)
inserted in listbox as a standalone column (flat 2D listbox structure in only lines and columns)
if he want it like this because than sorting and much more features available
in a standalone column (a reason why sometimes he want not to see a multilinetext in a column which contains the stringified object content (json).
In his listbox editor/designer he can define a string object adress (example: “myField.a.b.c.d.e.info”)
for a column by freeEdit or choose any of detected existing attribut in any existing record.

This is only a try to theoretically construct a use case,
real use cases are much more complex to explain exactly why
a path must sometimes integrated as string
and can not be hardcoded.
Yes, when i have 100 constant diffs
in only the object kind “product-type-B”
than it is maybe good to create some extra methods
in which only this diffs by a hardcoded syntax gets their respects.
But when i have >100 until endless (user-individual customizeable)
than this cannot put in a method in hardcoded way because attributPath must be dynamically.
Too when creating some generic routines,
in some cases this can only solved when dynPath inside the routine is possible.

I do something like this, but I would never store the user’s preference based as a property path string like “field.a.b.c.d.info”. I create the property path based on field references and then convert it to table and field numbers when storing it in the database. Then it still works if table or field names change.

Yes, it might be nice if 4D had a direct command to get the value at the end of a property path string. But with C_VARIANT it is not too difficult to write your own.

I hope you don’t mind, but perhaps the wrapper method could take advantage of PROCESS 4D TAGS.

// Method: yObjGetByPropertyPath
C_OBJECT($1)
C_TEXT($2;expression_t)
C_VARIANT($0;result_v)
$expression_t:="<!--#4DCODE result_v:=$1."+$2+" -->"
PROCESS 4D TAGS($expression_t;$expression_t;$1)
$0:=result_v

HTH,
Add

For developers who are dogmatically opposed to process variables:

C_OBJECT($1)
C_TEXT($2;$expression_t)
C_VARIANT($0;$result_v)

$expression_t:="<!--#4DCODE $2->:=$1."+$2+" -->"
PROCESS 4D TAGS($expression_t;$expression_t;$1;->$result_v)

$0:=$result_v

Not for production…just an exercise…

3 Likes

Ah yes, that’s even better Keisuke.

Thanks for your comment and suggest.
Just sometimes if i had no short answer, i do better no reply :wink:

And thanks for remind/point me at “PROCESS 4D TAGS”
i always do forgot (I didn’t even think about it) when i searching in the given possibilities,
but this command is so powerful
and in future i do more work+think with/on it.

But every use case is different and what is good in one group of situations
can be better done by other solution-way for a other kind of situation.

Sometimes i simulate a try with onErrCall
and somtimes i prefer in other situations
a different way to do this.

One point is to optional get a fix type back (receive a result).
Not always it is wished to say $variantVar is the receiver of result
of Process4DTags.
When the caller wished a fix type to receive
than it must guaranted that he becomes that.
Ok this can be done with another line of code after Process4DTags
to convert $variantVar into optional wished destination type with
OB Get(New object(“x”;$variantVar);“x”;Is Text).

Next point is, you have a attributPath a.b.c.d.myTxt
but not known if any object in the middle realy exists
so excute this fully path without saying
every path level in the middle must return a fix type object
can get maybe a type error in the middle (not only at the end by non exist myTxt).
Here is two posts from me to that:

I collect all this information and i am happy about all the hints, even if I still want to say something restrictive.
This does not mean that I do not gratefully collect the information given for me and learn from it.

or:

C_OBJECT($1)
C_TEXT($2)
C_VARIANT($0)
$0:=Formula from string("this."+$2).call($1)
2 Likes

Yes this is nice too.
I test these two alternatives in the next days
to inspect the positives and negatives arguments
to know which is for which situation maybe better one.
I test too which behavior i had too must hiding maybe possible errors
and i compare too how many ms M0/A1/A2 eats any of it by 100.000 executions in compiled-mode.

This ms seconds performance is totally relativ,
when i had a very big update-process over a very big data
than sometimes can happen this eats alone four hours,
than it is not relevant when i got instead of 4h than 4h+3000ms :wink:
But when a fast reaction is wished (feeled for user as realtime)
than it is different he got/see result in 100ms or in 3000ms.
So, from use case to use case, i prefer different ways…

This i did not test, it is only to show what can maybe needed too:

  // PM: "zObjGetByPropertyPathAlternativ"
  // zObjGetByPropertyPathAlternativ(object; propertyPath{; type}) // -> Function result
  // Example:
  // $obj:=New object("b";New object("b2";New object("b3";9)))
  // zObjGetByPropertyPathAlternativ($obj;"b.b2.b3";Is longint)  // -> 9

C_VARIANT($result;$0)
C_OBJECT($objSrc;$1)
C_TEXT($propertyPath;$2)
C_LONGINT($type;$3)

If (Count parameters>0)
	$objSrc:=$1
	If (Count parameters>1)
		$propertyPath:=$2
		If (Count parameters>2)
			$type:=$3
		End if 
	End if 
End if 

C_TEXT($methCurrent)
$methCurrent:=Method called on error
  // remember last set of onErrCall,
  // to restore the same state that existed before the this method called
ON ERR CALL("myOnErrHide")
// myOnErrHide shows no message but sets a intern errVar to receive in code if error happens

Case of 
	: (alternativA)
		C_TEXT($txtExpr)
		$txtExpr:="<!--#4DCODE $2->:=$1."+$propertyPath+" -->"
		PROCESS 4D TAGS($txtExpr;$txtExpr;$objSrc;->$result)
	: (alternativB)
		$result:=Formula from string("this."+$propertyPath).call($objSrc)
End case 

If (Count parameters>2)  // is any fix type wished?
	  // $3: destination type for result is defined, than try convert original-type to destination-type
	$result:=OB Get(New object("x";$result);"x";$type)
End if 

ON ERR CALL($methCurrent)  // Reinstallation of previous method

$0:=$result

  // - EOF -

To avoid having to create multiple process variables for the occasional use I adopted a convention of having a single object process variable I call prosObject and with v17+ manage it with this method:

//  prosObj
C_OBJECT(prosObject;$0)

If (Not(OB Is defined(prosObject)))
	prosObject:=New object
End if 

$0:=prosObject

There are a few cool things about this. First being you can use the method prosObj directly with dot notation:

	  // prosObject = undefined
	prosObj .x:=12345
	$x1:=prosObj .x  // prosObject = {x:12345} and  $x1 = 12345

This also works compiled. It is incredibly useful because unlike traditional process variables where you have the overhead of predeclaring them and problems with reusing them you can create and use prosObj on the fly.

Coupled with Process tags and adding $result to receive the result I can do:

	C_TEXT($code;$path;$expression_t)
	
	$o:=New object("a";New object("b";New object("c";"jackpot")))
	
	$path:="a.z"
	
	$expression_t:="<!--#4DCODE \r"  //  split into separate lines for clarity
	$expression_t:=$expression_t+"ON ERR CALL(\"ErrorSkip\")\r"  //  perhaps not strictly required
	$expression_t:=$expression_t+"prosObj.$result:=Null\r"  // if there is an error this value won't be changed
	$expression_t:=$expression_t+"prosObj.$result:=$1."+$path+" -->"
	PROCESS 4D TAGS($expression_t;$expression_t;$o)  // prosObject.$result = Null
	
	$path:="a.b.c"
	
	$expression_t:="<!--#4DCODE \r" 
	$expression_t:=$expression_t+"ON ERR CALL(\"ErrorSkip\")\r"
	$expression_t:=$expression_t+"prosObj.$result:=Null\r"
	$expression_t:=$expression_t+"prosObj.$result:=$1."+$path+" -->"
	PROCESS 4D TAGS($expression_t;$expression_t;$o)  // prosObject.$result = "jackpot"

This can run complied because there are no local variables. Since this example is in v17 I don’t return the result because it can be anything. In v18 I’d use C_VARIANT. (In v19 I’ll write a class for it.) And remember that you can use $ and _ in property names as this is ECMA compliant.

Finally, adding the ON ERR CALL may not be necessary - but it makes me feel more confident.