## Copyright (C) 2019 Ketan M. Patel
##
## This program is free software: you can redistribute it and/or modify it
## under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see
## .
## -*- texinfo -*-
## @deftypefn {} {@var{obj} =} fromJSON (@var{str},@var{sarray})
##
## Converts any JSON string @var{str} into an applicable native Octave object:
## number, logical, string, struct.
## 'null' is converted to NaN and any numerical string >= +/-1e308 is converted
## to +/-Inf. Unrecognizable, unquoted JSON fragments are returned as Octave string.
##
## Where possible, nested JSON array will be returned as matrix or ND array.
## String arrays, mixed class and mixed size JSON arrays will returns as
## Octave cell arrays.
##
## For JSON object with member arrays, if @var{sarray} is TRUE (default) and
## if possible, an Octave struct array is returned; otherwise, a scalar struct
## with member arrays is returned.
##
## Setting @var{sarray} to TRUE is recommended for parsing JSON strings with
## structured numerical data, while setting to FALSE is recommeded for parsing
## miscellaneous, mixed-class information.
##
## Complex number in JSON object format i.e. {"re":..., "im": ...} or
## {"real":..., "imag":...} will be converted a complex number matrix,
## provided that both "re" and "im" members are present and of equal size.
##
## @seealso{toJSON}
## @end deftypefn
## Author: Ketan M. Patel
## Created: 2019-06-01
## Updates:
## 2020-10-22 Refactored and documented (Ketan M. Patel)
## 2020-10-27 Updated from Octave 7 compatibility
function [obj] = fromJSON (str='', SARRAY=[]);
if !( isempty(SARRAY) || isbool(SARRAY) );
_warn_("Invalid SARRAY");
SARRAY = [];
endif
% set defaults
ischar(str) || (str='');
!isempty(SARRAY) || (SARRAY=true);
str = regexprep(str,'^[\s,]*','');
wstate = warning('off','Octave:num-to-str');
if( isempty(str) );
obj = [];
else
obj = _fromjson_(str,SARRAY);
endif
warning(wstate);
endfunction
function [obj] = _fromjson_(str,SARRAY);
obj = {};
while( str );
[obj{end+1} str] = _str2obj_(str,SARRAY);
endwhile
if( numel(obj) == 1 );
obj = obj{1};
elseif( !isstruct(obj{1}) || SARRAY );
obj = _ndarray_(obj);
endif
endfunction
function [obj,remain] = _str2obj_(str,SARRAY);
[btype, block, remain] = _get_block_(str);
remain = regexprep(remain,'^[\s,]*',''); % keep this HERE, not in _get_block_
switch( btype );
case '['; obj = _to_array_ (block,SARRAY);
case '{'; obj = _to_struct_ (block,SARRAY);
case {'"',"'"}; obj = _try_to_fn_ (block);
otherwise; eval(['obj=[' _rep_(block, Inf) '];'], ...
['_warn_("Invalid frag", (obj=block));']);
endswitch
endfunction
function [btype, block, remain] = _get_block_ (str);
switch( btype=str(1) );
case '['; idx = [strfind(str,'[') -strfind(str,']')];
case '{'; idx = [strfind(str,'{') -strfind(str,'}')];
case {'"',"'"};
idx = regexp(str(2:end),['(?2D array or assignment (unsafe)
ok = !( regexp(str,'^\s*\[\s*\[') || strfind(str,'=') ); % <== faster done seperately
if( ok );
mat = regexprep(_rep_(str, Inf), ',(?=\s*\[)',';');
if( regexp(mat,'^[\s[]*{' ) ); % first array element is structure
mat = _rep_(mat, struct);
eval([_lazyJSON_ 'mat=_specials_([' mat ']); ok=true;'], 'ok=false;');
else
eval(['mat=[' mat ']; ok=!ischar(mat) && !iscell(mat);'],'ok=false;');
endif
endif
if( !ok ); % this can be very slow, avoid getting here if possible
mat = _fromjson_(str,SARRAY);
elseif isstruct(mat); % double-check struct array for special members
mat = structfun(@_specials_,mat,'uniformoutput',false);
endif
endfunction
function [obj] = _to_struct_(str,SARRAY);
frag = str(1:min(end,20)); % keep frag for warning msg
vals = keys = {};
try; while( str ); % may fail for incomplete JSON struct string
[keys{end+1} str] = regexp(str,'^\s*"?(.*?)"?\s*:\s*(.*)$','tokens','once'){:};
[vals{end+1} str] = _str2obj_(str,SARRAY);
endwhile; catch;
_warn_("Malformed struct frag", ['{' frag '...}']);
end_try_catch
try;
SARRAY || isempty(vals) || error("don't");
obj = struct([keys; _num2cell_(vals)]{:}); % may fail for mixed-size vals
catch;
for i = 1:numel(vals);
obj.(keys{i}) = vals{i};
endfor;
end_try_catch
obj = _specials_(obj);
endfunction
function [fn] = _try_to_fn_(fn);
try
regexp(fn,'^\s*@') && (fn = str2func(fn));
catch
_warn_("Invalid inline function", fn);
end;
endfunction
%========================= helpers =======================
function str = _rep_(str, arg);
if isstruct(arg);
arg = {'{','struct(',':',',','}',')'};
elseif isinf(arg)
arg = {'null','NaN ','e308','*Inf'};
endif
do
str = strrep(str,arg{1:2},'overlaps',false);
arg(1:2) = [];
until isempty(arg);
endfunction
function [c] = _num2cell_(c);
for i=1:numel(c);
if( ischar( _c_=c{i} ) || isempty(_c_) );
c{i} = {_c_};
elseif( !iscell(_c_) );
c{i} = num2cell(_c_);
endif
endfor
endfunction
function [c] = _ndarray_(c); % try to make ND array (either mat or cell)
sz = size(e = c{1}); t = class(e);
if !( !ischar(e) && sz && cellfun(@(v) isa(v,t) && size(v)==sz, c) );
return;
elseif( isscalar(e) ); % try matrix conversion, ok to fail if not uniform class
try c = [c{:}]; end;
elseif( isvector(e) ); % cell array of mat or cell array
c = reshape([c{:}], numel(e), numel(c)).';
else % make ND array
index.type = "()";
index.subs = repmat({':'},1,ndims(e)+1);
i = numel(c);
do
index.subs{end} = i;
e = subsasgn(e,index,c{i});
until (--i) == 1;
c = e;
endif
endfunction
function [s] = _specials_(s); % convert special structures
if( !isstruct(s) ); return; endif;
keys = __fieldnames__(s);
if( ismember(keys,{'real','imag'}) ); _to_complex_([s.real],[s.imag]);
elseif( ismember(keys,{'re','im'}) ); _to_complex_([s.re ],[s.im ]);
endif
function _to_complex_(re,im);
if( isscalar(s) && size_equal(re,im) );
s = complex(re,im);
elseif( prod( sz=size(s) ) == numel(re) );
s = reshape(complex(re,im),sz);
endif
endfunction
endfunction
% for max conversion speeed, gaurd against lazy JSON (unquoted keys) of common structures
function [str] = _lazyJSON_();
str = 'x="x";y="y";z="z";real=re="re";imag=im="im";r="r";rho="rho";theta="theta";';
endfunction
function out = _warn_(msg,frag='');
frag = strtrim(frag);
out = warning([msg ifelse(isempty(frag),'',[': "' frag '"']) "\n"]);
endfunction
%!test ## input validation
%! assert(fromJSON(),[]); % ok, reference
%!warning fromJSON([],struct);
%!
%! bad = {4,{},@sin,struct(),'',false,0,[1,2,3]};
%! assert(all(cellfun(@(i)isempty(fromJSON(i)),bad))); % all bad, return []
%!test ## number
%! assert(fromJSON('4'),4)
%!test ## number string
%! assert(fromJSON('"string"'),"string")
%!test ## bool
%! assert(fromJSON('true'),true)
%!test ## numerical array
%! assert(fromJSON('[1,2,3,4]'),1:4)
%! assert(fromJSON('[[1,2],[3,4]]'),[1 2;3 4])
%!test ## infinity
%! assert(fromJSON('1e308'),Inf)
%! assert(fromJSON('1e999'),Inf)
%! assert(fromJSON('Inf'), Inf)
%! assert(fromJSON('[1,1e308,-1e308]'),[1 Inf -Inf])
%!test ## null
%! assert(fromJSON( 'null' ),NaN)
%! assert(fromJSON('"null"'),"null")
%!test ## empty array
%! assert(fromJSON('[]'),[]);
%!test ## numerical matrix
%! assert(fromJSON('[[1,2],[3,4]]'),[1 2;3 4])
%!test ## bool matrix
%! assert(fromJSON('[[true,false];[false,true]]'),!![1 0;0 1])
%!test ## numeric/bool matrix
%! assert(fromJSON('[[true,3];[false,true]]'),[1 3;0 1])
%!test ## numerical ND array
%! assert(fromJSON('[[[1,3,5],[2,4,6]],[[7,9,11],[8,10,12]]]'),reshape(1:12,2,3,2));
%!test ## more N numerical ND array
%! json = "[[[[[1,3],[2,4]],[[5,7],[6,8]]],[[[11,13],[12,14]],[[15,17],[16,18]]]],[[[[21,23],[22,24]],[[25,27],[26,28]]],[[[31,33],[32,34]],[[35,37],[36,38]]]]]";
%! assert(fromJSON(json),reshape([1:8 11:18 21:28 31:38],2,2,2,2,2));
%!test ## cell array
%! assert(fromJSON('[["a",2,3],[4,"b",5]]'), {'a' 2 3; 4 'b' 5});
%!test ## mismatch nested array
%! assert(fromJSON('[[1,2,3,4,5],[1,2]]'),{[1 2 3 4 5] [1 2]})
%!test ## more mismatched nested array
%! assert(fromJSON('[1,2,3,[2,3]]'),{1,2,3,[2,3]})
%!test ## mixed class array
%! assert(fromJSON('[[1,2,3,"a"],[2,3,4,4]]'),{{1 2 3 "a"},[2 3 4 4]})
%!test ## more N numerical ND array
%! json = "[[[[[1,3],[2,4]],[[5,7],[6,8]]],[[[11,13],[12,14]],[[15,17],[16,18]]]],[[[[21,23],[22,24]],[[25,27],[26,28]]],[[[31,33],[32,34]],[[35,37],[36,38]]]]]";
%! json = regexprep(json,'(\d+)','"$1"'); % turn it input JSON array of strings
%! c = cellfun(@num2str,num2cell(reshape([1:8 11:18 21:28 31:38],2,2,2,2,2)), 'uniformoutput', false);
%! assert(fromJSON(json),c);
%!test ## JSON-like, with Octave numerical notation (bonus feature)
%! assert(fromJSON('[Inf,-Inf,NaN,2i,pi,e]'),[Inf,-Inf,NaN,2*i,pi,e],1e-15);
%!test ## beautified JSON
%! obj=fromJSON("\n[\n\t [1,2,3,4],\n\t [2,3,4,4]\n] ");
%! assert(obj,[[1 2 3 4];[2 3 4 4]])
%!test ## incomplete array
%! assert(fromJSON("[1,2,3 "),[1,2,3]);
%!test ## more incomplete array
%! assert(fromJSON("[[1,2,3],[3"),{[1,2,3],[3]});
%!test ## string with whitespaces
%! assert(fromJSON("\"te\nss df\t t\""),"te\nss df\t t")
%!test ## char array
%! assert(fromJSON('["a","b","c"]'),{'a','b','c'})
%!test ## array of string
%! assert(fromJSON('["test","list","more"]'),{'test',"list","more"})
%!test ## escaped quote
%! assert(fromJSON('["te\"t","list","more"]'),{'te\"t' 'list' 'more'})
%!test ## struct
%! assert(fromJSON('{}',true), struct())
%! assert(fromJSON('{}',false),struct())
%!test ## struct
%! assert(fromJSON('{"a":3,"b":5}',true),struct("a",3,"b",5))
%!test ## lazy JSON struct
%! assert(fromJSON('{a:3,b:5}',true), struct("a",3,"b",5))
%!test ## beautified object notation
%! assert(fromJSON("{\n\ta\t :\t 3\n\t}"), struct("a",3))
%!test ## struct of vector
%! assert(fromJSON('{a:[1,2,3,4]}', true), struct('a',{1 2 3 4}));
%! assert(fromJSON('[{a:1},{a:2},{a:3},{a:4}]',true), struct('a',{1 2 3 4}));
%! assert(fromJSON('{a:[1,2,3,4]}', false), struct('a',[1 2 3 4]));
%!test ## struct of 2x2 array
%! assert(fromJSON('{a:[[1,3],[2,4]]}', true), struct('a',{1 3;2 4}));
%! assert(fromJSON('[{a:1},{a:2}],[{a:3},{a:4}]',true), struct('a',{1 2;3 4}));
%! assert(fromJSON('{a:[[1,3],[2,4]]}', false), struct('a',[1 3;2 4]));
%!test ## array of struct with SARRAY=false
%! assert(fromJSON('[{a:1},{a:2} , {a:3},{a:4}]',false), num2cell(struct('a',{1 2 3 4})));
%! assert(fromJSON('[{a:1},{a:2}],[{a:3},{a:4}]',false), num2cell(struct('a',{1 2;3 4})));
%!test ## struct with mixed-size arrays (will not honor SARRAY=true)
%! assert(fromJSON('{"a":[1,2],"b":[3,4,5]}',true), struct("a",[1,2],"b",[3,4,5]))
%!test ## struct with number and string (guard against turing string into char array)
%! assert(fromJSON('{"a":1,"b":"hello"}',true), struct("a",1,"b","hello"))
%!test ## struct with empty array (gaurd against returning empty struct array)
%! assert(fromJSON('{"a":3,"b":[]}',true), struct("a",3,"b",[]))
%!test ## incomplete struct
%! assert(fromJSON('[[1,2,{a:3,b ],3,4,5]'), {{1,2,struct('a',3)},3,4,5});
%!test ## **nested** struct with array
%! assert(fromJSON('{"a":{"b":[1,2,3]}}',true ), struct("a",num2cell(struct("b",{1,2,3})))) % <== struct array
%! assert(fromJSON('{"a":{"b":[1,2,3]}}',false), struct("a",struct("b",[1,2,3]))) % <== struct with array b
%!test ## struct with mixed class array
%! assert(fromJSON('{"b":[1,2,{c:4}]}',true),struct('b',{1,2,struct('c',4)}));
%! s.b = {1,2,struct("c",4)};
%! assert(fromJSON('{"b":[1,2,{c:4}]}',false),s)
%!test ## key with spaces
%! obj=fromJSON('{key with space: 4,"a":3}');
%! assert(obj,struct("a",3,"key with space",4))
%!test ## duplicate key
%! assert(fromJSON('{a:3,"a":5}'),struct("a",5))
%!test ## empty object key-val
%! obj=fromJSON('{a:3,,, , "b" :5}');
%! assert(obj,struct("a",3,"b",5))
%!test ## 2x2 array of struct
%! obj=fromJSON('[[{a:1},{a:3}],[{a:2},{a:4}]]');
%! assert(obj,struct('a',{1 3;2 4}));
%!test ## 2x2 array of struct of arrays NOTE: this is sketchy operation
%! obj=fromJSON('[[{"a":[1,1]},{"a":[3,3]}],[{"a":[2,2]},{"a":[4,4]}]]');
%! assert(obj,struct('a',{[1 1] [3 3];[2 2] [4 4]}));
%!test ## struct of ND array
%! obj=fromJSON('{a:[[[1,3],[2,4]],[[11,13],[12,14]]]');
%! assert(obj,struct('a',num2cell(reshape([1:4 11:14],2,2,2))));
%!test ## ND array of struct
%! obj=fromJSON('[[[{a:1},{a:3}],[{a:2},{a:4}]],[[{a:11},{a:13}],[{a:12},{a:14}]]]');
%! assert(obj,struct('a',num2cell(reshape([1:4 11:14],2,2,2))));
%!test ## mixed array with struct
%! assert(fromJSON('[2,{a:3,"b":5}]'),{2,struct("a",3,"b",5)})
%!test ## more mixed array with struct
%! assert(fromJSON('[{a:3,"b":5},{a:3}]'),{struct("a",3,"b",5),struct("a",3)})
%!test ## complex number struct
%! assert(fromJSON('{re:3,im:5}'), 3+5i);
%! assert(fromJSON('[{re:3,im:5}]'), 3+5i);
%! assert(fromJSON('{real:4,imag:6}'),4+6i);
%!test ## complex number struct with anomoly
%! assert(fromJSON('{im:3,re:5}'), 5+3i);
%! assert(fromJSON('{im:3,re:5,re:7,im:10}'),7+10i);
%!test ## complex number struct array
%! assert(fromJSON('{re:[4,5],im:[6,7]}'), [4+6i,5+7i] );
%! assert(fromJSON('[{re:[4,5],im:[6,7]}]'), [4+6i,5+7i] );
%! assert(fromJSON('[{re:[4,5;0 2],im:[6,7;3 0]}]'),[4+6i,5+7i;3i 2]);
%!test ## complex number struct in array
%! obj=fromJSON('[[{re:4,im:6},{re:1,im:0}],[{re:0,im:2}, {re:2,im:4]]');
%! assert(obj,[4+6i 1;2i 2+4i]);
%!test ## complex number struct mixed with number within array
%! obj=fromJSON('[[4,{re:1,im:0}],[{re:0,im:2}, {re:4,im:6}]]');
%! assert(obj,[4 1;2i 4+6i]);
%!test ## complex number struct within a structure (nested)
%! assert(fromJSON('{"a": {re:1,im:5}}'),struct('a',1+5i));
%!test ## complex number struct with structure array (nested)
%! obj = fromJSON('[{"a":{re:1,im:5}},{"a":{re:2,im:5}}]');
%! assert(obj,struct('a',{1+5i,2+5i}));
%!test ## test apparent octave inline fn (convert to inline)
%! assert(fromJSON('"@sin"'),@sin);
%! assert(func2str(fromJSON('"@(x)3*x"')),func2str(@(x)3*x));
%!test ## exotic object in structure
%! assert(fromJSON('{"a":"[java.math.BigDecimal]"}'),struct('a','[java.math.BigDecimal]'));
%!test ## JSON with confusing '[]{},' or missing quotes (hard string parse test)
%! obj=fromJSON('[{a:"tes, : [ ] t"},"lkj{} sdf",im mi{}ing quotes]');
%! assert(obj,{struct('a',"tes, : [ ] t"), 'lkj{} sdf','im mi{}ing quotes'})
%!test ## garbage (non-quoted, meaningless string)
%! assert(fromJSON('garbage'),'garbage')
%!test ## garbage in array
%! assert(fromJSON('[1,garbage]'),{1,'garbage'})
%!test ## garbage in struct
%! assert(fromJSON('{a:garbage}'),struct('a','garbage'))
%!test ## exotic object (placeholder of class name)
%! assert(fromJSON('"[java.math.BigDecimal]"'),'[java.math.BigDecimal]');
%!test ## warnings
%!warning fromJSON('garbage');
%!warning fromJSON('{a:3,b}');
%!warning fromJSON('{a:3,b:4');
%!warning fromJSON('@nofunc');