Is there a way to correctly recognize boolean values ​​for JSON stringifying?

Get help with using AutoHotkey (v1.1 and older) and its commands and hotkeys
vmech
Posts: 356
Joined: 25 Aug 2019, 13:03

Is there a way to correctly recognize boolean values ​​for JSON stringifying?

Post by vmech » 28 Mar 2021, 21:54

Hi there!
Is there a way to correctly recognize boolean values ​​for JSON stringifying ? E.g.:

Code: Select all

{"keyIntOne":1,"keyBoolTrue":true,"keyIntTwo":0,"keyBoolFalse":false}
It is perfectly parsing into internal object's structures, but trying to stringify does this:

Code: Select all

{"keyIntOne":1,"keyBoolTrue":1,"keyIntTwo":0,"keyBoolFalse":0}
or this:

Code: Select all

{"keyIntOne":true,"keyBoolTrue":true,"keyIntTwo":false,"keyBoolFalse":false}
PS. I tried to use a bunch of AHK scripts from this forum and github repos for parse JSON into objects before and stringify objects into JSON after. And even modified some to correctly recognize null and numbers without quotes. But no script was able to stringify booleans correctly :(

May be stringify AHK objects into ECMA-compatible JSON is just impossible ?

User avatar
mikeyww
Posts: 26884
Joined: 09 Sep 2014, 18:38

Re: Is there a way to correctly recognize boolean values ​​for JSON stringifying?

Post by mikeyww » 28 Mar 2021, 22:16

In AHK, True and False are special terms that represent 1 and 0, respectively. In other words, "True" does not mean "True" or true; it means 1. You could probably handle them specifically and convert them (1 and 0) into words, if that is what is needed.

Code: Select all

state := False
word := state ? "True" : "False"
MsgBox, %word%

vmech
Posts: 356
Joined: 25 Aug 2019, 13:03

Re: Is there a way to correctly recognize boolean values ​​for JSON stringifying?

Post by vmech » 29 Mar 2021, 05:23

mikeyww wrote:
28 Mar 2021, 22:16
In AHK, True and False are special terms that represent 1 and 0, respectively. In other words, "True" does not mean "True" or true; it means 1. You could probably handle them specifically and convert them (1 and 0) into words, if that is what is needed.

Code: Select all

state := False
word := state ? "True" : "False"
MsgBox, %word%
I know this my friend, I read the docs too :)

In general, I came to a similar conclusion: set custom values ​​for boolean variables in an object during JSON parsing, make the necessary changes to object, taking into account these "features", and then try to output correct values ​​back into JSON.
Such a step, of course, will put a bold cross on the universality of such a solution. But this only means that all publicly available decisions on converting JSON -> AHK -> JSON are just a fiction.

In addition, there is another way: after all, JSON is, in fact, just a text string ;)
Please post your script code inside [code] ... [/code] block. Thank you.

User avatar
mikeyww
Posts: 26884
Joined: 09 Sep 2014, 18:38

Re: Is there a way to correctly recognize boolean values ​​for JSON stringifying?

Post by mikeyww » 29 Mar 2021, 06:58

OK. It sounds like you have it figured out! I don't see it as such a problem, just an adjustment in the scripting.

User avatar
Chunjee
Posts: 1419
Joined: 18 Apr 2014, 19:05
Contact:

Re: Is there a way to correctly recognize boolean values ​​for JSON stringifying?

Post by Chunjee » 29 Mar 2021, 19:08

Looked into this as I used the json libraries in some projects. I didn't see an obvious way to fix because ahk true is indistinguishable from 1. You could write some custom stuff to parse it special like a string "true" get's written as true. But even this is challenging because how do you know [0, 1, 2] in ahk memory should take your special value or not.

vmech
Posts: 356
Joined: 25 Aug 2019, 13:03

Re: Is there a way to correctly recognize boolean values ​​for JSON stringifying?

Post by vmech » 30 Mar 2021, 01:07

Chunjee wrote:
29 Mar 2021, 19:08
Looked into this as I used the json libraries in some projects. I didn't see an obvious way to fix because ahk true is indistinguishable from 1. You could write some custom stuff to parse it special like a string "true" get's written as true. But even this is challenging because how do you know [0, 1, 2] in ahk memory should take your special value or not.
Isn't the answer obvious? Don't try to use AHK built-in true/false to recognize imported from JSON true/false, use custom objects, for example bool := {"true":"true","false":"false"}. And subsequently in the program, after importing, and before exporting to JSON, operate only with these values ​​- this guarantees the correct export of boolean variables into JSON.

PS. This step is only necessary if you plan to export AHK objects to JSON, otherwise you will not be able to receive ECMA-compliant JSON.
If you plan to import JSON into AHK, and only work with objects in the program, without further exporting to JSON, then there is no need to take such a step - import "as is" works correctly.
Please post your script code inside [code] ... [/code] block. Thank you.


WarpRider
Posts: 6
Joined: 25 Jun 2020, 09:03

Re: Is there a way to correctly recognize boolean values ​​for JSON stringifying?

Post by WarpRider » 31 Jan 2023, 11:07

I was modifyed the lib AutoHotkey-JSON from cocobelgica. Now the internal ahk vars like true, false and the string null will be untouch. Now you can read and wirte json-files without chances on the tree things.
My Test JSON:
{"rawtrue":true,"rawfalse":false,"onenull":null,"numberten":10}
Please test the lib.

Code: Select all

/**
 * Lib: JSON.ahk
 *     JSON lib for AutoHotkey.
 * Version:
 *     v2.1.3 [updated 04/18/2016 (MM/DD/YYYY)]
 * License:
 *     WTFPL [http://wtfpl.net/]
 * Requirements:
 *     Latest version of AutoHotkey (v1.1+ or v2.0-a+)
 * Installation:
 *     Use #Include JSON.ahk or copy into a function library folder and then
 *     use #Include <JSON>
 * Links:
 *     GitHub:     - https://github.com/cocobelgica/AutoHotkey-JSON
 *     Forum Topic - http://goo.gl/r0zI8t
 *     Email:      - cocobelgica <at> gmail <dot> com
 *
 * Modify by WarpRider, Member on https://www.autohotkey.com/boards
 *     ingnore the ahk internal vars true/false and the string null wil be not empty
 */


/**
 * Class: JSON
 *     The JSON object contains methods for parsing JSON and converting values
 *     to JSON. Callable - NO; Instantiable - YES; Subclassable - YES;
 *     Nestable(via #Include) - NO.
 * Methods:
 *     Load() - see relevant documentation before method definition header
 *     Dump() - see relevant documentation before method definition header
 */
class JSON
{
	/**
	 * Method: Load
	 *     Parses a JSON string into an AHK value
	 * Syntax:
	 *     value := JSON.Load( text [, reviver ] )
	 * Parameter(s):
	 *     value      [retval] - parsed value
	 *     text    [in, ByRef] - JSON formatted string
	 *     reviver   [in, opt] - function object, similar to JavaScript's
	 *                           JSON.parse() 'reviver' parameter
	 */
	class Load extends JSON.Functor
	{
		Call(self, ByRef text, reviver:="")
		{
			this.rev := IsObject(reviver) ? reviver : false
		; Object keys(and array indices) are temporarily stored in arrays so that
		; we can enumerate them in the order they appear in the document/text instead
		; of alphabetically. Skip if no reviver function is specified.
			this.keys := this.rev ? {} : false

			static quot := Chr(34), bashq := "\" . quot
			     , json_value := quot . "{[01234567890-tfn"
			     , json_value_or_array_closing := quot . "{[]01234567890-tfn"
			     , object_key_or_object_closing := quot . "}"

			key := ""
			is_key := false
			root := {}
			stack := [root]
			next := json_value
			pos := 0

			while ((ch := SubStr(text, ++pos, 1)) != "") {
				if InStr(" `t`r`n", ch)
					continue
				if !InStr(next, ch, 1)
					this.ParseError(next, text, pos)

				holder := stack[1]
				is_array := holder.IsArray

				if InStr(",:", ch) {
					next := (is_key := !is_array && ch == ",") ? quot : json_value

				} else if InStr("}]", ch) {
					ObjRemoveAt(stack, 1)
					next := stack[1]==root ? "" : stack[1].IsArray ? ",]" : ",}"

				} else {
					if InStr("{[", ch) {
					; Check if Array() is overridden and if its return value has
					; the 'IsArray' property. If so, Array() will be called normally,
					; otherwise, use a custom base object for arrays
						static json_array := Func("Array").IsBuiltIn || ![].IsArray ? {IsArray: true} : 0
					
					; sacrifice readability for minor(actually negligible) performance gain
						(ch == "{")
							? ( is_key := true
							  , value := {}
							  , next := object_key_or_object_closing )
						; ch == "["
							: ( value := json_array ? new json_array : []
							  , next := json_value_or_array_closing )
						
						ObjInsertAt(stack, 1, value)

						if (this.keys)
							this.keys[value] := []
					
					} else {
						if (ch == quot) {
							i := pos
							while (i := InStr(text, quot,, i+1)) {
								value := StrReplace(SubStr(text, pos+1, i-pos-1), "\\", "\u005c")

								static tail := A_AhkVersion<"2" ? 0 : -1
								if (SubStr(value, tail) != "\")
									break
							}

							if (!i)
								this.ParseError("'", text, pos)

							  value := StrReplace(value,  "\/",  "/")
							, value := StrReplace(value, bashq, quot)
							, value := StrReplace(value,  "\b", "`b")
							, value := StrReplace(value,  "\f", "`f")
							, value := StrReplace(value,  "\n", "`n")
							, value := StrReplace(value,  "\r", "`r")
							, value := StrReplace(value,  "\t", "`t")

							pos := i ; update pos
							
							i := 0
							while (i := InStr(value, "\",, i+1)) {
								if !(SubStr(value, i+1, 1) == "u")
									this.ParseError("\", text, pos - StrLen(SubStr(value, i+1)))

								uffff := Abs("0x" . SubStr(value, i+2, 4))
								if (A_IsUnicode || uffff < 0x100)
									value := SubStr(value, 1, i-1) . Chr(uffff) . SubStr(value, i+6)
							}

							if (is_key) {
								key := value, next := ":"
								continue
							}
						
						} else {
							value := SubStr(text, pos, i := RegExMatch(text, "[\]\},\s]|$",, pos)-pos)
							;MsgBox, "value=" %value%
							
							static number := "number", integer :="integer"
							if value is %number%
							{
								if value is %integer%
									value += 0
							}
							
							;WarpRider 31.01.2023: hier wird value auf true oder false geprüft und behandelt, nach AHK wird das dann 0 oder 1,
							;das ist aber falsch, da true/false für JSON keine boolschen Variablen sind, value muss unverändert übernommen werden
							else if (value == "true" || value == "false")
								value := value	;ORIGINAL: value := %value% + 0
							
							
							else if (value == "null")
								value := "null"									;WarpRider 31.01.2023: hier genauso, warum wird null nicht stur übernommen?
							else
							; we can do more here to pinpoint the actual culprit
							; but that's just too much extra work.
								this.ParseError(next, text, pos, i)

							pos += i-1
						}

						next := holder==root ? "" : is_array ? ",]" : ",}"
					} ; If InStr("{[", ch) { ... } else

					is_array? key := ObjPush(holder, value) : holder[key] := value

					if (this.keys && this.keys.HasKey(holder))
						this.keys[holder].Push(key)
				}
			
			} ; while ( ... )

			return this.rev ? this.Walk(root, "") : root[""]
		}

		ParseError(expect, ByRef text, pos, len:=1)
		{
			static quot := Chr(34), qurly := quot . "}"
			
			line := StrSplit(SubStr(text, 1, pos), "`n", "`r").Length()
			col := pos - InStr(text, "`n",, -(StrLen(text)-pos+1))
			msg := Format("{1}`n`nLine:`t{2}`nCol:`t{3}`nChar:`t{4}"
			,     (expect == "")     ? "Extra data"
			    : (expect == "'")    ? "Unterminated string starting at"
			    : (expect == "\")    ? "Invalid \escape"
			    : (expect == ":")    ? "Expecting ':' delimiter"
			    : (expect == quot)   ? "Expecting object key enclosed in double quotes"
			    : (expect == qurly)  ? "Expecting object key enclosed in double quotes or object closing '}'"
			    : (expect == ",}")   ? "Expecting ',' delimiter or object closing '}'"
			    : (expect == ",]")   ? "Expecting ',' delimiter or array closing ']'"
			    : InStr(expect, "]") ? "Expecting JSON value or array closing ']'"
			    :                      "Expecting JSON value(string, number, true, false, null, object or array)"
			, line, col, pos)

			static offset := A_AhkVersion<"2" ? -3 : -4
			throw Exception(msg, offset, SubStr(text, pos, len))
		}

		Walk(holder, key)
		{
			value := holder[key]
			if IsObject(value) {
				for i, k in this.keys[value] {
					; check if ObjHasKey(value, k) ??
					v := this.Walk(value, k)
					if (v != JSON.Undefined)
						value[k] := v
					else
						ObjDelete(value, k)
				}
			}
			
			return this.rev.Call(holder, key, value)
		}
	}

	/**
	 * Method: Dump
	 *     Converts an AHK value into a JSON string
	 * Syntax:
	 *     str := JSON.Dump( value [, replacer, space ] )
	 * Parameter(s):
	 *     str        [retval] - JSON representation of an AHK value
	 *     value          [in] - any value(object, string, number)
	 *     replacer  [in, opt] - function object, similar to JavaScript's
	 *                           JSON.stringify() 'replacer' parameter
	 *     space     [in, opt] - similar to JavaScript's JSON.stringify()
	 *                           'space' parameter
	 */
	class Dump extends JSON.Functor
	{
		Call(self, value, replacer:="", space:="")
		{
			this.rep := IsObject(replacer) ? replacer : ""

			this.gap := ""
			if (space) {
				static integer := "integer"
				if space is %integer%
					Loop, % ((n := Abs(space))>10 ? 10 : n)
						this.gap .= " "
				else
					this.gap := SubStr(space, 1, 10)

				this.indent := "`n"
			}
			
			;MsgBox, % "vorreturn=" this.Str({"": value}, "")
			return this.Str({"": value}, "")
		}

		Str(holder, key)
		{
			value := holder[key]

			if (this.rep)
				value := this.rep.Call(holder, key, ObjHasKey(holder, key) ? value : JSON.Undefined)

			if IsObject(value) {
			; Check object type, skip serialization for other object types such as
			; ComObject, Func, BoundFunc, FileObject, RegExMatchObject, Property, etc.
				static type := A_AhkVersion<"2" ? "" : Func("Type")
				if (type ? type.Call(value) == "Object" : ObjGetCapacity(value) != "") {
					if (this.gap) {
						stepback := this.indent
						this.indent .= this.gap
					}

					is_array := value.IsArray
				; Array() is not overridden, rollback to old method of
				; identifying array-like objects. Due to the use of a for-loop
				; sparse arrays such as '[1,,3]' are detected as objects({}). 
					if (!is_array) {
						for i in value
							is_array := i == A_Index
						until !is_array
					}

					str := ""
					if (is_array) {
						Loop, % value.Length() {
							if (this.gap)
								str .= this.indent
							
							v := this.Str(value, A_Index)
							str .= (v != "") ? v . "," : "null,"
						}
					} else {
						colon := this.gap ? ": " : ":"
						for k in value {
							v := this.Str(value, k)
							if (v != "") {
								if (this.gap)
									str .= this.indent

								str .= this.Quote(k) . colon . v . ","
							}
						}
					}

					if (str != "") {
						str := RTrim(str, ",")
						if (this.gap)
							str .= stepback
					}

					if (this.gap)
						this.indent := stepback

					return is_array ? "[" . str . "]" : "{" . str . "}"
				}
			
			}
			;WarpRider 31.01.2023: alle Werte hier, ausser Zahlen werden durch die Funktion Quote() mit " eingefasst,
			;das darf bei true,false,null eben nicht so sein, da true/false für JSON keine boolschen Variablen sind und null nicht leer werden
			else ; is_number ? value : "value"
			{
			;MsgBox, vor.Str.return.raw.value=%value%
			if (value == "true" || value == "false" || value == "null")
			  return value
			else
			  return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value)
			}
				
		}

		Quote(string)
		{
		;MsgBox, start.Quote.string=%string%
		
			static quot := Chr(34), bashq := "\" . quot

			if (string != "") {
				  string := StrReplace(string,  "\",  "\\")
				; , string := StrReplace(string,  "/",  "\/") ; optional in ECMAScript
				, string := StrReplace(string, quot, bashq)
				, string := StrReplace(string, "`b",  "\b")
				, string := StrReplace(string, "`f",  "\f")
				, string := StrReplace(string, "`n",  "\n")
				, string := StrReplace(string, "`r",  "\r")
				, string := StrReplace(string, "`t",  "\t")

				static rx_escapable := A_AhkVersion<"2" ? "O)[^\x20-\x7e]" : "[^\x20-\x7e]"
				while RegExMatch(string, rx_escapable, m)
					string := StrReplace(string, m.Value, Format("\u{1:04x}", Ord(m.Value)))
			}
			;MsgBox, % "vor.Quote.return=" quot . string . quot
			return quot . string . quot
		}
	}

	/**
	 * Property: Undefined
	 *     Proxy for 'undefined' type
	 * Syntax:
	 *     undefined := JSON.Undefined
	 * Remarks:
	 *     For use with reviver and replacer functions since AutoHotkey does not
	 *     have an 'undefined' type. Returning blank("") or 0 won't work since these
	 *     can't be distnguished from actual JSON values. This leaves us with objects.
	 *     Replacer() - the caller may return a non-serializable AHK objects such as
	 *     ComObject, Func, BoundFunc, FileObject, RegExMatchObject, and Property to
	 *     mimic the behavior of returning 'undefined' in JavaScript but for the sake
	 *     of code readability and convenience, it's better to do 'return JSON.Undefined'.
	 *     Internally, the property returns a ComObject with the variant type of VT_EMPTY.
	 */
	Undefined[]
	{
		get {
			static empty := {}, vt_empty := ComObject(0, &empty, 1)
			return vt_empty
		}
	}

	class Functor
	{
		__Call(method, ByRef arg, args*)
		{
		; When casting to Call(), use a new instance of the "function object"
		; so as to avoid directly storing the properties(used across sub-methods)
		; into the "function object" itself.
			if IsObject(method)
				return (new this).Call(method, arg, args*)
			else if (method == "")
				return (new this).Call(arg, args*)
		}
	}
}


Post Reply

Return to “Ask for Help (v1)”