Network Transparent Boolean Logic Evaluator

7

0

Write an expression evaluator for boolean expressions encoded as JSON. Read on for explanation and rules.

Motivation:

Consider a scenario where two agents (Alice and Bob) are communicating with each other under the following constraints:

  • All communication must be done as valid JSON strings (no JSONP or extensions), because Bob and Alice love JSON. They have had problems with character encoding in the past, so they have decided to restrict themselves to ASCII.
  • Alice needs Bob to notify her in case a certain condition is met, but:
    • Bob doesn't know what the condition is in advance.
    • Alice can't evaluate the condition herself because it requires information that only Bob has.
    • The status of the variables can change rapidly enough (and the network is slow enough) that it would be infeasible for Bob to continuously transmit the data to Alice.
  • Neither knows anything about the implementation of the other agent (for example, they can't make assumptions about the implementation language of the other agent).

Alice and Bob have worked out a protocol to help them complete their task.

Representing Conditional Expressions:

  1. They have agreed to represent Boolean literals by strings, for example "foo" or "!bar", where ! represents the negation of a literal, and valid names contain only letters and numbers (case sensitive, any strictly positive length; "" and "!" are not valid; i.e. a literal is a string whose contents match the regex /^!?[a-zA-Z0-9]+$/).

  2. Expressions are only transmitted in negation normal form. This means that only literals can be negated, not subexpressions.

  3. Expressions are normalized as a conjunction of disjunctions of conjunctions of disjunctions ... etc encoded as nested arrays of strings. This needs a bit more explanation, so...

Expression Syntax:

Consider the following boolean expression:

(¬a ∧ b ∧ (c ∨ ¬d ∨ e ∨ ¬f) ∧ (c ∨ (d ∧ (f ∨ ¬e))))

Notice that the entire expression is contained in parentheses. Also notice that the first level(and all odd levels) contains only conjunctions(∧), the second level(and all even levels) contains only disjunctions(∨). Also notice that no subexpression is negated. Anyway meat and potatoes... This would be represented by Alice in JSON as:

["!a","b",["c","!d","e","!f"],["c",["d",["f","!e"]]]]

Another example:

((a ∨ b ∨ c))

Becomes:

[["a","b","c"]]

In summary: [] are interpreted as parentheses, literals are wrapped in "",! means negation, and , means AND or OR depending on nesting depth.

NB: the empty conjunction (the result of ANDing no variables) is true, and the empty disjunction (the result of ORing no variables) is false. They are both valid boolean expressions.

Bob's data:

Bob's data is emitted by a program on his computer as a simple JSON object as key-value pairs, where the key is the name of the variable, and the value is either true or false. For example:

{"a":false,"b":true,"c":false}

Bob needs our help!

Bob has little programming experience, and he's in over his head. He has handed you the task.

You need to write a program that accepts the JSON text that Alice sent along with the JSON text for Bob's data, and evaluates the expression with those values, returning true or false (the actual strings true and false; Bob and Alice love JSON, remember?)

  • Bob says less is more, so he wants you to make your source code as short as possible (this is a code-golf)

  • You are allowed to use an external library to parse the JSON text, but this is the only exception. Only standard library functionality is allowed.

  • Note that the JSON is accepted and returned as a string. There is no requirement to explicitly parse it though, if you can get it to always work without doing so.

  • If Alice uses a variable that Bob doesn't know, assume it is false. Bob's data may contain variables that Alice doesn't use.

  • If Alice's or Bob's JSON Object contains an invalid literal, or the JSON is invalid, or it contains a non ASCII character, then your program must return null

Some test cases(in JSON):

[
 {"alice":"[\"\"]","bob":"{}","result":"null"},
 {"alice":"[\"!\"]","bob":"{}","result":"null"},
 {"alice":"[\"0\"]","bob":"{\"0\":true}","result":"true"},
 {"alice":"[]","bob":"{}","result":"true"},
 {"alice":"[[]]","bob":"{}","result":"false"},
 {"alice":"[\"a>\"]","bob":"{\"a\":true}","result":"null"},
 {"alice":"[\"0\"]","bob":"{}","result":"false"},
 {"alice":"[\"0\"]","bob":"","result":"null"},
 {"alice":"[\"1\"]","bob":"{\"1\":false}","result":"false"},
 {"alice":"[\"true\"]","bob":"{\"true\":false}","result":"false"},
 {"alice":"[\"foo\",\"bar\",\"baz\"]","bob":"{\"foo\":true,\"bar\":true,\"biz\":true}","result":"false"},
 {"alice":"[[[\"a\",[\"!b\",\"c\"],\"!c\"],\"d\"]]","bob":"{\"a\":true,\"b\":false,\"c\":true,\"d\":false}","result":"false"},
 {"alice":"[\"!a\",\"b\",[\"c\",\"!d\",\"e\",\"!f\"],[\"c\",[\"d\",[\"f\",\"!e\"]]]]","bob":"{\"a\":false,\"b\":true,\"c\":true,\"e\":true,\"f\":false}","result":"true"}
]

As can be seen from the test cases, input and output are strings that are correctly parseable JSON. The empty string test case for bob was originally in error, the correct value to return is null, but if it saves you characters to return false, then that is also acceptable. I added cases for the empty clauses to make that explicit. I added another 2 test cases in response to a bug I found in someone's implementation

Tim Seguine

Posted 2014-02-01T12:41:13.130

Reputation: 824

Hmm... in most programming languages 0 is not a valid identifier name. Is it deliberate that you include it nevertheless? – John Dvorak – 2014-02-01T14:44:43.033

@JanDvorak yes, :) I thought it might make certain strategies more difficult. – Tim Seguine – 2014-02-01T14:45:14.927

"If Alice's or Bob's JSON Object contains an invalid literal, or the JSON is invalid, or it contains a non ASCII character, then your program should return null" - are we required to not accept invalid inputs (including invalid variable names)? This could cost us extra bytes. – John Dvorak – 2014-02-01T14:47:10.173

@JanDvorak yes. the result then should be the text null. Do you have a suggestion for a clearer formulation? – Tim Seguine – 2014-02-01T14:48:38.080

Saying "must" should suffice. – John Dvorak – 2014-02-01T14:49:22.390

@JanDvorak edited to reflect that change – Tim Seguine – 2014-02-01T14:50:21.693

How are both parts of the input separated? Can we assume both JSONs are single-line and separated by a newline? – John Dvorak – 2014-02-01T14:53:38.900

1@JanDvorak Oops, I clicked chat accidentally. I chose to leave that open to interpretation. Yours is a reasonable interpretation. As long as you can unambiguously determine which part is which, it is okay. – Tim Seguine – 2014-02-01T14:55:41.980

Is an array as Bob's data a valid input? – John Dvorak – 2014-02-01T15:39:30.977

@JanDvorak the rules say key-value pairs – Tim Seguine – 2014-02-01T15:40:19.567

that's true, but [a, b, c] is semantically equivalent to {"0":a, "1":b, "2":c} – John Dvorak – 2014-02-01T15:42:15.910

@JanDvorak yeah, but the variables can have arbitrary names. You would have to decide on a canonical ordering for them in advance. You have to accept what Bob gave you. He is your boss. – Tim Seguine – 2014-02-01T15:43:50.750

2

@JanDvorak, there was an opportunity to ask these questions in the sandbox.

– Peter Taylor – 2014-02-01T19:25:41.430

@PeterTaylor to be fair, I didn't leave it there for very long. – Tim Seguine – 2014-02-01T23:23:19.747

What about empty arrays? Do they have to be / can they be / mustn't they be supported? []=>true, [[]]=>false – John Dvorak – 2014-02-02T09:28:45.603

{"alice":"[\"0\"]","bob":"","result":"false"} - wait, what? The empty string is not a valid JSON, much less a valid object JSON. – John Dvorak – 2014-02-02T09:41:46.867

@JanDvorak oops you are right. I added that one last minute. I changed the response for that test case to null – Tim Seguine – 2014-02-02T13:13:53.543

@JanDvorak The normal mathematical convention is that the empty disjunction is defined as being false and that the empty conjunction is defined as being true. I will add test cases for these and an explanation, your guess at the truth values is right – Tim Seguine – 2014-02-02T13:17:34.667

6> Bob has little programming experience, and he's in over his head. :( – Bob – 2014-02-02T17:40:13.893

@Bob aw, you should prove Tim wrong. – FireFly – 2014-02-02T18:08:39.413

1@Bob Definitely you should prove me wrong. Tell Alice I said hi. – Tim Seguine – 2014-02-02T19:07:54.873

Answers

5

ECMASCript 6 variation, 213 194 190 189 192 166 chars

try{P=_=>JSON.parse(prompt())
B=P()
f=(a,c)=>a[c?'some':'every'](v=>v.big?[B][+/\W|_/.test(s=v.slice(C=v[0]=='!'))-!s][s]^C:f(v,!c))
q=f(P())}catch(_){q=null}alert(q)

Note: takes Bob's input first, and then Alice's input. This saves us four bytes.

  • Checks for syntactically valid JSON since JSON.parse throws if it isn't.
  • Since we're already relying on the try-catch for the above, we (ab)use out-of-bounds access to trigger our "failed validation" path when validating the (possibly negated) literals
    • This is accomplished with the [B][...][s] part (thanks, @l4m2! used to use a slightly more verbose construction)
    • The -!s is to catch "" and "!" as invalid literals
    • The C variable holds the "should negate" state, which is accomplished with a "bitwise" XOR (and relies on implicit type coercion)

ES5 solution: https://codegolf.stackexchange.com/revisions/19863/8

FireFly

Posted 2014-02-01T12:41:13.130

Reputation: 7 107

If you have an ungolfed version, it would be nice to see. – Tim Seguine – 2014-02-02T19:13:18.737

1@TimSeguine added one. – FireFly – 2014-02-02T19:48:57.183

One problem I found. It accepts the empty string as a valid literal name {"alice":"[\"\"]","bob":"{}"} gives false, but by the rules null is the correct answer – Tim Seguine – 2014-02-02T19:59:15.950

@Tim good call, I forgot to check for that. Should work now. – FireFly – 2014-02-02T20:06:00.447

B["z"[expr].x,s] => [B][expr][s]? – l4m2 – 2018-06-03T23:32:43.017

@l4m2 Good catch! Incorporating this, and also taking a closer look at this since it's ancient... – FireFly – 2018-07-14T15:31:28.467

3

Ruby, 335 (327?) characters

begin;a,b=*$<;require'json';b=JSON[b];q if a=~/{|}/||!a[?[]||b.any?{|v,x|v=~/[\W_]/||x!=!!x}
l=0;a.gsub!(/\[|\]/){"[{}]"[($&==?]?2:0)+l=1-l]};a.gsub!(/"(!)?([a-z\d]+)"/i){b[$2]^$1}
0 while [[/\[[#{f=!1},\s]*\]/,f],[/{[#{t=!f},\s]*?}/,t],[/\[[\w,\s]*\]/,t],[/{[\w,\s]*}/,f]].any?{|k,v|a.sub!(k){v}}
rescue a=??;end;puts a=~/\W/?"null":a

I've heard you can't parse HTML with Regex, but I've never been told you cannot parse JSON this way, so I tried.

Alice's input is separated from Bob's input by a newline; also, it's assumed that neither input contains newlines itself. This assumption is for I/O purposes only, and is not used later. If I can make an extra assumption that Alice's input contains no whitespace, we can drop 8 characters by removing all \s's on line #3. If we can only assume it contains no whitespace but spaces, we can still replace \s's with the space characters to save 4 characters.

John Dvorak

Posted 2014-02-01T12:41:13.130

Reputation: 9 048

Well JSON also contains the Dyck language, so it is also not regular. Even the restricted form of JSON that the problem specifies contains arbitrarily nested matched `[]. It really depends on how you are using it though. I can't read the golfed version well enough to tell very much of what is going on. – Tim Seguine – 2014-02-02T13:10:03.637

Okay as a ruling on the whitespace issue. You are essentially in Bob's position, so in particular you don't know how Alice is going to format her whitespace. For all you know, she ran it through a pretty printer first. – Tim Seguine – 2014-02-02T13:43:05.310

3

Python 3 (429)

Longer than the other answers, but arguably the most readable so far... :)

import json,sys,re
r,p,m='^!?[a-zA-Z\d]+$',print,re.match
def g(a,b,s):
 if type(a)is str:
  if m(r,a):
   try:return('not '+str(b[a[1:]]))if a[0]=='!'else str(b[a])
   except:return'False'
  raise Exception
 return'('+((' and 'if s else' or ').join(g(x,b,not s)for x in a))+')'
s,j=sys.argv,json.loads
try:
 a,b = j(s[1]),j(s[2]or'{}');
 if all(m(r,x)and x[0]!='!'for x in b):p(str(eval(g(a,b,1))).lower())
except:
 p('null')

Joachim Isaksson

Posted 2014-02-01T12:41:13.130

Reputation: 1 161

2

Node.JS (331 335 310 296)

As a starting point, manually minified, could definitely be improved a bit; it basically just "brute force" evaluates the expression;

As a golf hack restriction, the variable y can not be defined or it won't work :)

r=/^[a-z\d]+$/i;function g(a,b,s,c){c=s;for(k in a){if((k=a[k]).trim){k=k.slice(i=k[0]=='!');
if(!k.match(r))y;k=i?!b[k]:b[k]}else{k=g(k,b,!s)}c=s?c&k:c|k}return!!c;}z=process.argv;a=z[2]
;b=z[3];try{p=JSON.parse;l=console.log;a=p(a);b=p(b);for(k in b)if(!k.match(r))y;l(g(a,b,1))}
catch(e){l(null)}

Called with alice as first argument, bob as second argument, outputs result to stdout.

EDIT: Shrunk the code a little with the help of @FireFly and by removing support for "empty bob".

Joachim Isaksson

Posted 2014-02-01T12:41:13.130

Reputation: 1 161

"You need to write a program that ..." - this means, you must include the I/O. Writing a function is not sufficient. – John Dvorak – 2014-02-02T10:36:24.360

@JanDvorak Fixed – Joachim Isaksson – 2014-02-02T13:17:00.717

I removed the empty bob test case because that was clearly wrong. Sorry for that oversight. – Tim Seguine – 2014-02-02T13:24:07.440

substringslice saves a few chars. You could also save four chars by turning k and c into globals (i.e. remove them from the param list). – FireFly – 2014-02-02T15:04:45.840

@FireFly Thanks, incorporated your suggestions and removed support for the "broken" test case. – Joachim Isaksson – 2014-02-02T15:25:50.937

1

JavaScript, 178 bytes

Optimized from Tim Seguine's answer

try{P=JSON.parse,p=prompt;A=P(p());B=P(p());f=(a,c)=>a.map(v=>v.big?[B][/\W|_/.test(s=v.replace(/^!/,''))-!s][s]^s>v:f(v,!c))[c?'some':'every'](P);q=f(A)}catch(_){q=null}alert(q)

Try it online!

l4m2

Posted 2014-02-01T12:41:13.130

Reputation: 5 985