Based on some comments here in SLACK. I have attem...
# cfml-general
m
Based on some comments here in SLACK. I have attempted to not build JSON by hand (@Adam Cameron) and instead built a structure first and then used serializeJSON, BUT..... I want to preserve case of the key/names and specify a numeric data type at the same time. And I get an error when doing that.
Copy code
<cfset date = "05/10/2023">
<cfset hoursPerDay = 4.0> <!--- should be numeric --->
<cfset data = $[Schedule_Date = date, Schedule_Hours = hoursPerDay]> <!--- I want Schedule_Date and Schedule_Hours to be case sensitive not converted to uppercase ---> <!--- shortcut for case sensitive structure ${ --->
<cfset metadata = {Schedule_Hours:"numeric"}>
<cfset data.setMetadata(metadata)>
<cfoutput>
#serializeJSON(data)#
</cfoutput>
get this error:
Copy code
An exception occurred while calling the function setMetaData.
Cannot cast coldfusion.runtime.OrderedCaseSensitiveStruct to coldfusion.runtime.Struct
If you want me to show the non-implicit struct format and/or script based method I can, but get the same error regardless. You can not serialize a case sensitive structure and set the metadata to be numeric at the same time. Also this is in a component and the hoursPerDay is passed in as a numeric cfargument, it is not a simple 4.0 shown above.
s
I've never seen that
$[]
syntax before. What happens if you do:
Copy code
someStruct = {
 'Schedule_Date' : date,
   'Schedule_Hours' : hoursPerDay
}
There is a CF admin setting for preserving case in serialization, but in my experience even if it's off, if you do
'attr' : 'value'
instead of
attr : 'value'
you'll get the serialization you want
t
Yeah, I was about to reply with the same thing, when this got deleted... It might also work with
data["Schedule_Date"] = date
. I think the important part is making sure that the struct keys are referenced as strings.
m
regarding ${ If you look on https://helpx.adobe.com/coldfusion/developing-applications/the-cfml-programming-lang[…]sing-arrays-and-structures/creating-and-using-structures.html under Case sensitive structs I can not change the CFAdmin setting. I will look at quoting the keys!!!
t
But I'm a fan of the
{"key": value}
syntax because it starts out as basically JSON already.
👍🏻 2
s
what exactly are you trying to do with that
setMetadata
line, also?
t
that forces cf to treat the number as a numer instead of a string.
s
interesting
t
it's a thing.... i forget which version added it.
m
so the issue is the hourperday in JSON should be a number not a quoted string
s
haven't used ACF since 11 so this all seems like a rather long way around. If I had to do this I'd use mementifier and mappers
m
That's the same function, but I see if the bug exists that way
t
yeah, that was a reference for sam.
m
yeah still get an error
t
It does look like you can set a key name in the metadata though
Schedule_Hours:{type: "numeric", name:"Schedule_Hours"}
m
@sknowlton yeah it seems a long way around, building a structure first then serialize to JSON, but was trying to do the best practice. I would have been done a couple of days ago building a simple JSON from scratch. But wanted to try the "Best Practice".
@Tim interesting
s
OK, I get it. "best practice" is kind of a religious term with some of this stuff. Lucee doesn't (yet) support
setMetaData
which is arguably Lucee's problem and not yours, but when possible, one best practice is to avoid engine-specific code (though it seems like they might tackle it for Lucee 6) so maybe forget about this consideration for now this seems like domain logic, so it should probably be in a service layer and in script rather than in tags; or in a model. are you using a framework? if not, you're already bit far afield from best practices, though probably somebody will fight me on that
having said all that, 'I want this struct serialized in exactly this way' can be fiddly with all versions of CF so it may not be an ideal best practice hill to die on
t
do you really need to ordered struct? I think that might be where it's choking.
m
out of curiosity, what doesn't seem to work as you expect from?
data = {"Schedule_Date": date, "Schedule_Hours": hoursPerDay}
✔️ 1
t
as soon as I make data a normal unordered struct, everything works fine.
s
yeah I'd ditch the ordered struct here
sounds like Adobe hasn't quite finished implementing
setMetaData
either if only some structs and not others allow it
m
Well I got it working now through the quoted keys and not setting the structure to case sensitive. Thanks Guys!! But you can't use both Case sensitive structs and setMetaData in ACF
fwiw, it also works without the metadata... hours perday still ends up a number.
m
@Tim yeah in this simple example it does but if through a component argument some how it does not preserve the datatype
m
if you need to coerce that, i'd use "Schedule_Hours": javaCast("float",hoursPerDay)
👍🏻 1
or whichever javaCast() meets your need
m
<cfargument name="hoursPerDay" type="numeric" required="true">
a
All the code here seems to unnecessarily complicated. This works fine and covers your requirements on all versions of CFML engines on trycf.com, other than Lucee losing some precision on the numeric value - but even that is not contrary to the JSON spec. https://trycf.com/gist/8ae7aac7cb9d3c497211cf360a89748a/acf?theme=monokai
Copy code
<cfset date = "05/10/2023">
<cfset hoursPerDay = 4.0> <!--- should be numeric --->
<cfset data = {"Schedule_Date" = date, "Schedule_Hours" = hoursPerDay}>
<cfoutput>
#serializeJSON(data)#
</cfoutput>
You've clocked the thing about using strings to create case-sensitive identifiers; but if yer value is numeric, if will serialise as numeric without any dicking around with metadata. More seriously though,
DD|MM/MM|DD/YYYY
is not a great string format to use as a date. I presume that's
mm/dd/yyyy
as there are more USAns here than anyone else, but that format is idiosyncratic to USAn humans. It's not really a date format used for anything else. Nothing sane anyhow ;-). Yer better off using some ISO-8601 format for date->string formatting, or perhaps RFC-822. Or, indeed, use an actual Date in the CFML code, and let the CFML engine take care of it for you. That is all part of the serialisation process as well.
NB: well done for deciding to use the inbuilt JSON handling stuff rather than string manipulation for yer JSON.
m
@Adam Cameron just a simple example. The date format is actually yyyy-mm-dd that will be going into production.
1
b
@Mark Berning Just to touch on the struct set metadata thing, I would forget that ever existed and burn it in a fire 😆 That was Adobe's terribly-implemented attempt at making up for their crap legacy behavior of storing all simple values as strings internally which I documented here https://www.codersrevolution.com/blog/why-is-lucee-so-much-better-at-handling-json The thing is, Adobe completely reworked how simple values are stored internally in CF 2018 (numbers stay "real" numbers and booleans stay "real" booleans) and the struct metadata junk became unecessary. All you need do today is simply create the CFML data structure with the appropriate types and the JSON follows appropriately with no extra effort.
a
Better articulated that me. @Mark Berning what's a better example of the numeric thing that started you down the trail of thinking you needed to handle it specially? There might be some nuance we're missing.
m
I am building a function in a CFC that returns JSON, the value in question comes from a argument that its type is numeric, however when that variable is then used to build the structure, the value is converted to a string in the JSON output. I am old school and still using tag format (bash if you'd like), so maybe that has something to do with it. The JSON will eventually be passed to an API that I don't own and is wanting the value to be numeric. Its not finished yet but here is the code.
hoursPerDay
should be numeric in the JSON output (no quotes).
Copy code
<cffunction  name="genSchedule" access="public" output="true" returntype="struct" >
		<cfargument  name="startdate" type="date" required="true">
		<cfargument  name="endDate" type="date" required="true">
		<cfargument  name="FF_Project_Id" type="string" required="true">
		<cfargument  name="hoursPerDay" type="numeric" required="true" default="8.0">
		<cfargument  name="FF_Contact_Id" type="string" required="false" default="">
		<cfargument  name="Webkey_Id" type="string" required="true">
		<cfargument  name="AdvDates" type="string" required="false" default="">
		<cfargument  name="EventID" type="numeric" required="true">

		<cfset error ="">
		<cfset startdate= parseDateTime(arguments.startdate)>
		<cfset enddate= parseDateTime(arguments.enddate)>
		<cfset ddiff = DateDiff("d",startdate,enddate)+1>
		<cfset dayoffset = 1-DayOfWeek(startdate)>
		<cfset wdiff = Week(enddate)-Week(startdate)+1+((Year(enddate)-Year(startdate))*53)>

		<cfset result = StructNew("casesensitive")>
		<cfset week = StructNew()>
		<cfset Schedules = ArrayNew(1)>
		<cfset AdvancedDates = ArrayNew(1)>
		<cfset metadata = {Schedule_Hours:"numeric"}>
		<cftry>
		<cfif arguments.AdvDates eq "">
			<cfloop index="wk" from="1" to="#wdiff#">
				<cfset wkStart ="">
				<cfset wkEnd ="">
				<cfset cnt = 0>
				<cfset totHours = 0>

				<cfloop index="day" from="1" to="7">
					<cfset date=dateFormat(DateAdd('d',dayoffset,startdate),"yyyy-mm-dd")>
					<cfif date gte startdate AND date lte enddate AND IsValid("range",DayOfWeek(date),2,6)>
						<cfif wkStart eq ""><cfset wkStart=date> </cfif>
						<cfset wkEnd = date>
						<cfset data = ['Schedule_Date' = date, 'Schedule_Hours' = arguments.hoursPerDay]>
						<cfset data.setMetadata(metadata)>
						<cfset totHours += arguments.hoursPerDay>
						<cfset AdvancedDates[++cnt] = data>
						<cfset Schedules[wk]['AdvancedDates'] = AdvancedDates>
					</cfif>
					<cfset dayoffset++>
				</cfloop>
				<cfif wkStart neq "">
					<cfset Schedules[wk]['End_Date'] = wkEnd>
					<cfset Schedules[wk]['Start_Date'] = wkStart>
					<cfset Schedules[wk]['FF_Project_Id'] = arguments.FF_Project_Id>
					<cfset Schedules[wk]['Start_Date'] = wkStart>
					<cfset Schedules[wk]['Total_Hours'] = totHours>
					<cfset Schedules[wk]['EventID'] = arguments.EventID>
				</cfif>
			</cfloop>
		<cfelse>
			<cfloop list="#arguments.AdvDates#" item="wk" delimiters=";" index="w">
				<cfset wkStart ="">
				<cfset wkEnd ="">
				<cfset cnt = 0>
				<cfset totHours = 0>
				<cfloop list="#wk#" item="date">
					<cfset date = dateFormat(date,"yyyy-mm-dd")>
					<cfif wkStart eq ""><cfset wkStart=date> </cfif>
					<cfset wkEnd = date>
					<cfset data = ['Schedule_Date' = date, 'Schedule_Hours' = arguments.hoursPerDay]>
					<cfset data.setMetadata(metadata)>
					<cfset totHours += arguments.hoursPerDay>
					<cfset AdvancedDates[++cnt] = data>
					<cfset Schedules[w]['AdvancedDates'] = AdvancedDates>
				</cfloop>
				<cfif wkStart neq "">
					<cfset Schedules[w]['End_Date'] = wkEnd>
					<cfset Schedules[w]['Start_Date'] = wkStart>
					<cfset Schedules[w]['FF_Project_Id'] = arguments.FF_Project_Id>
					<cfset Schedules[w]['Start_Date'] = wkStart>
					<cfset Schedules[w]['Total_Hours'] = totHours>
					<cfset Schedules[w]['EventID'] = arguments.EventID>
				</cfif>
			</cfloop>
		</cfif>


		<cfset retJSON = ${"Webkey_Id":arguments.Webkey_Id,"FF_Contact_Id":arguments.FF_Contact_Id}>
		<cfset retJSON.Schedules = Schedules>
		<cfset result.data = retJSON>
		<cfset result.data_JSON = serializeJSON(retJSON)>
		<cfcatch>
			<cfset error = cfcatch.message>
		</cfcatch>
		</cftry>


		<cfset result.error = error>

		<cfreturn result>
	</cffunction>
The other thing I wanted was the keys to be cased correctly and I solved that with the quoting them, Thanks - I forgot about that nuance.
For some reason without the setMetadata function
hoursPerDay
becomes a quoted value and not a numeric value during serializeJSON function.
t
i think the issue is likely that the numeric value is instantiated as a string, and just passing it to the function doesn't change its type. It can be a string, and still be numeric. Where does the passed value come from originally?
👍 1
a
Hey Mark, that's not how to show some a case to reproduce a situation! Most of that code is not relevant to what we're talking about, and the important detail will be obscured by the stuff that's not important / doesn't contribute. Give http://sscce.org/ a read. What we're after is the fewest lines of code that demonstrate what yer seeing. And this will seldom be something copy and pasted from your code base. Based on your description, I think this demonstrates what you mean: https://trycf.com/gist/a2a591ef1010f8909c2f00218c2ffcb9/lucee5?theme=monokai
m
it looks that way
so I could just do the old
var*1
or cast it
b
a
Even with your default value,
"8.0"
is interpreted as a string. You need to make sure the values are actually numeric (ie: ints or floats as appropriate). But this does answer your question. I still agree with Brad though: forget the metadata, and just coerce the value into a numeric when sticking the value into the struct
👍🏻 1
@bdw429s even with floats,
val
would work wouldn't it?
b
But some sort of numeric math like
num+0
will do it too. I actually do this in CFConfig when writing WDDX
Yes, you are correct Adam. I shouldn't have said "int" 🙂
a
I think
val
is the more emphatic / clear approach here. There's no "why's he multiplying this by one/adding zero?"
(I did actually go and check... I backed you more than I backed the CF Dev Team 😉)
m
Awesome thanks guys. Maybe someday I will teach this old dog new tricks and write in script.
b
https://cfscript.me/ can be a great place to get a leg up on converting bits
t
not having to type
<cfset
every line is so worth it. 🙂
a
@Mark Berning you do not know the pain and suffering you caused me, when I wrote that repro case in tags for you 😜
🤣 3
m
@bdw429s yeah I looked at that and it did not do everything correctly (but not expecting it either). It does not like the loop on list with an index - but not bad.
a
Yeah it completely ballsed-up a
<cfmail>
call I fed it the other day. Meant to raise an issue with... PeteF?... about that. I reckon it's definitely good for 80% of stuff though.
👍🏻 1