├── .circleci
├── config.yml
└── markdown
├── .gitignore
├── .markdownlint.json
├── LICENSE
├── README.md
├── blawx
├── README.md
└── beard.blawx
├── doc
├── are-you-eligible-for-exemption.png
├── concept-model.png
├── do-you-have-a-beard.png
└── is-your-facial-hair-prohibited.png
├── json-logic
├── README.md
├── beard_definition.json
└── test
│ ├── beard_definition_test.js
│ └── logic.js
├── legislation.pdf
├── python-authorityspoke
├── .gitignore
├── README.md
├── conftest.py
├── example_data
│ ├── codes
│ │ └── beard_tax_act.xml
│ └── holdings
│ │ └── beard_rules.json
├── requirements-dev.txt
├── requirements.txt
├── statute_rules.ipynb
└── test_rules.py
├── python
├── .gitignore
├── .python-version
├── README.md
├── run.py
└── test.py
├── ruby
├── Gemfile
├── Gemfile.lock
├── README.md
├── beard_checker.rb
├── facial_hair.rb
└── test
│ ├── beard_checker_test.rb
│ └── facial_hair_test.rb
└── shared
├── example_beards.csv
├── example_beards.json
└── example_beards.yml
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 2
3 | jobs:
4 | python:
5 | docker:
6 | - image: circleci/python:3.7.2
7 | steps:
8 | - checkout
9 | - run:
10 | name: Python Tests
11 | command: >
12 | cd python; ./test.py
13 | - run:
14 | name: AuthoritySpoke Tests
15 | command: |
16 | cd python-authorityspoke
17 | python3 -m venv venv
18 | . venv/bin/activate
19 | pip install authorityspoke
20 | pip install pytest
21 | pytest
22 | ruby:
23 | docker:
24 | - image: circleci/ruby:2.6.3-node
25 | steps:
26 | - checkout
27 | - run: gem install bundler
28 | - run:
29 | name: Install dependencies
30 | command: cd ruby; bundle install
31 | - run:
32 | name: Ruby Tests
33 | command: >
34 | cd ruby; bundle exec ruby ./test/beard_checker_test.rb
35 | jsonlogic:
36 | docker:
37 | - image: circleci/node:9.9.0
38 | steps:
39 | - checkout
40 | - run:
41 | name: JSON Logic Tests
42 | command: >
43 | cd json-logic/test; node beard_definition_test.js
44 | markdownlint:
45 | docker:
46 | - image: circleci/ruby:2.6.3-node
47 | steps:
48 | - checkout
49 | - run:
50 | name: markdown lint
51 | command: ./.circleci/markdown
52 |
53 | yamllint:
54 | docker:
55 | - image: circleci/python:3.7.2
56 | steps:
57 | - checkout
58 | - run:
59 | name: Install yamllint
60 | command: sudo pip install --upgrade yamllint
61 | - run:
62 | name: Yaml linter
63 | command: yamllint .
64 |
65 | workflows:
66 | version: 2
67 | build_all:
68 | jobs:
69 | - ruby
70 | - python
71 | - jsonlogic
72 | - yamllint
73 | - markdownlint
74 |
--------------------------------------------------------------------------------
/.circleci/markdown:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -euv
4 |
5 | sudo npm install -g markdownlint-cli
6 |
7 | FILES=$(find . -name '*.md' | grep -v node_modules)
8 | for file in $FILES
9 | do
10 | echo "============== checking $file"
11 | markdownlint "$file"
12 | done
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.mypy_cache/
2 | .pytest_cache/
3 | node_modules/**
4 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "MD013": false
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Service Integration team for the New Zealand Goverment
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Example rules as code
2 |
3 | This repo is for implementations of the made-up [Australian Beard Tax
4 | (Promotion of Enlightenment Values) Act 1934](legislation.pdf).
5 |
6 | Its purpose is to show the breadth of options in writing legislation as code,
7 | explore some of the pros and cons of different options, and to give you a
8 | starting point for encoding real legislation.
9 |
10 | Implementations are for the following decision tree:
11 |
12 | 
13 |
14 | The legislation reads:
15 |
16 | ```legislation
17 | In this Act, beard means any facial hair no shorter than 5 millimetres in
18 | length that:
19 | a. occurs on or below the chin, or
20 | b. exists in an uninterrupted line from the front of one ear to the front of
21 | the other ear below the nose.
22 | ```
23 |
24 | ## How to contribute
25 |
26 | Please add implementations of the decision trees and tests in other languages
27 | and formats.
28 |
29 | - Add a new folder in the root directory.
30 | - Create a README.md with details about how to run the code.
31 | - Make a pull request!
32 |
33 | You can use the examples included in the `shared` directory to write tests and
34 | make sure your code passes.
35 |
36 | ### CI
37 |
38 | A YAML and [Markdown linter](https://github.com/DavidAnson/markdownlint) are run
39 | on Circle CI. You can run the markdown linter locally with:
40 |
41 | ```sh
42 | ./.circleci/markdown
43 | ```
44 |
45 | You may also like to add a job to run your tests on Circle CI by
46 | adding it to `.circleci/config.yml`.
47 |
--------------------------------------------------------------------------------
/blawx/README.md:
--------------------------------------------------------------------------------
1 | # Blawx Implementation
2 |
3 | Please note that the public alpha version of Blawx is unstable. It is very possible that this implementation will stop working when
4 | changes are made to the app. If that happens, let @Gauntlet173 on GitHub know.
5 |
6 | ## Usage
7 |
8 | Download the `beard.blawx` file in this folder.
9 | Go to the [Blawx Live Alpha version](https://www.blawx.com/alpha.html), start the app.
10 | Choose "Menu", "Load Workspace" and upload the `beard.blawx` file.
11 |
12 | Then choose "Menu", and "Run Blawx Code" to run the query to determine if the tests passed.
13 |
14 | ## Details
15 |
16 | The Blawx implementation uses actual lengths for the beard instead of a true/false variable
17 | indicating whether the length is 5mm or more.
18 |
19 | The test data in the shared folder of the repository has been re-implemented in a data block
20 | inside the Blawx Workspace.
21 |
22 | For more details, see the [blog post at Blawx.com](https://www.blawx.com/2020/05/testing-your-rules-as-code/).
23 |
--------------------------------------------------------------------------------
/blawx/beard.blawx:
--------------------------------------------------------------------------------
1 | teststestID1length4chinFALSEnoseTRUEbeardedFALSEtestID2length4chinFALSEnoseFALSEbeardedFALSEtestID3length4chinTRUEnoseFALSEbeardedFALSEtestID4length4chinTRUEnoseTRUEbeardedFALSEtestID5length6chinFALSEnoseTRUEbeardedTRUEtestID6length6chinFALSEnoseFALSEbeardedFALSEtestID7length6chinTRUEnoseFALSEbeardedTRUEtestID8length6chinTRUEnoseTRUEbeardedTRUEtestID9length6chinTRUEbeardedTRUEtestID10length6noseTRUEbeardedTRUEis_beardedpersonTRUEpersonpersonlengthgtelength5personTRUEpersonTRUEnot_beardedpersonFALSEpersonpersonTRUEPersonhas_a_beardfacial_hair_length_in_mmfacial_hair_below_chinfacial_hair_ear_to_ear_below_noseTestexpected_resultpassedtest_person_existstest(?ID)expected_resulttesttestIDtestIDbeardedtestexpected_resultadd_beard_length_to_testlengthtesttestIDtestIDlengthtestlengthadd_chin_to_testchintesttestIDtestIDchintestchinadd_nose_to_testnosetesttestIDtestIDnosetestnosepassedtestTRUEtesttestAtestAfailedtestFALSEtesttestAtestBneqABtesttestpassed
--------------------------------------------------------------------------------
/doc/are-you-eligible-for-exemption.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BetterRules/example-rules-as-code/c0ef52c936c754086421a3d659b03f9c69079830/doc/are-you-eligible-for-exemption.png
--------------------------------------------------------------------------------
/doc/concept-model.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BetterRules/example-rules-as-code/c0ef52c936c754086421a3d659b03f9c69079830/doc/concept-model.png
--------------------------------------------------------------------------------
/doc/do-you-have-a-beard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BetterRules/example-rules-as-code/c0ef52c936c754086421a3d659b03f9c69079830/doc/do-you-have-a-beard.png
--------------------------------------------------------------------------------
/doc/is-your-facial-hair-prohibited.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BetterRules/example-rules-as-code/c0ef52c936c754086421a3d659b03f9c69079830/doc/is-your-facial-hair-prohibited.png
--------------------------------------------------------------------------------
/json-logic/README.md:
--------------------------------------------------------------------------------
1 | # JSON Logic
2 |
3 | This encodes the rule about beards in
4 | a [JSON Logic](http://jsonlogic.com/) format.
5 |
6 | JSON Logic can be parsed in many different languages -
7 | see [their website](http://jsonlogic.com/) for more details.
8 | The tests are written using the
9 | [JS parser](https://github.com/jwadhams/json-logic-js/),
10 | which has been pulled down into the `logic.js` file.
11 |
12 | The logic is in `beard_definition.json`.
13 | See `test/beard_definition_test.js` for an example of how to call it.
14 |
15 | To run tests:
16 | `node test/beard_definition_test.js`
17 |
--------------------------------------------------------------------------------
/json-logic/beard_definition.json:
--------------------------------------------------------------------------------
1 | {
2 | "and" : [
3 | {"==" : [ { "var" : "facial_hair_over_limit" }, "true" ]},
4 | { "or": [
5 | {"==" : [ { "var" : "on_or_below_chin" }, "true" ] },
6 | {"==" : [ { "var" : "uninterrupted_below_nose" }, "true" ] }
7 | ]
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/json-logic/test/beard_definition_test.js:
--------------------------------------------------------------------------------
1 | const jsonLogic = require("./logic");
2 | const beardExamples = require("../../shared/example_beards.json");
3 | const beardRules = require("../beard_definition.json");
4 |
5 | const beardData = beardExamples.map(function(beard){
6 | let data = {}
7 |
8 | data.on_or_below_chin = beard.facial_hair_on_or_below_chin
9 | data.uninterrupted_below_nose = beard.facial_hair_uninterrupted
10 | data.facial_hair_over_limit = beard.facial_hair_over_5mm
11 | data.outcome = beard.outcome
12 | return(data)
13 | })
14 |
15 | let passed_tests = [];
16 | let failed_tests = [];
17 |
18 | beardData.forEach(function(beard){
19 | let result = jsonLogic.apply(beardRules, beard)
20 | if(String(result) == String(beard.outcome)){
21 | console.log("✅")
22 | passed_tests.push(beard)
23 | } else {
24 | console.log("❌")
25 | failed_tests.push(beard)
26 | }
27 | })
28 |
29 | console.log(passed_tests.length + ' tests passed')
30 | console.log(failed_tests.length + ' tests failed')
31 |
32 | failed_tests.forEach(function(test){
33 | console.log('Failed this beard:')
34 | console.log(test)
35 | });
36 |
--------------------------------------------------------------------------------
/json-logic/test/logic.js:
--------------------------------------------------------------------------------
1 | /* globals define,module */
2 | /*
3 | Using a Universal Module Loader that should be browser, require, and AMD friendly
4 | http://ricostacruz.com/cheatsheets/umdjs.html
5 | */
6 | ;(function(root, factory) {
7 | if (typeof define === "function" && define.amd) {
8 | define(factory);
9 | } else if (typeof exports === "object") {
10 | module.exports = factory();
11 | } else {
12 | root.jsonLogic = factory();
13 | }
14 | }(this, function() {
15 | "use strict";
16 | /* globals console:false */
17 |
18 | if ( ! Array.isArray) {
19 | Array.isArray = function(arg) {
20 | return Object.prototype.toString.call(arg) === "[object Array]";
21 | };
22 | }
23 |
24 | /**
25 | * Return an array that contains no duplicates (original not modified)
26 | * @param {array} array Original reference array
27 | * @return {array} New array with no duplicates
28 | */
29 | function arrayUnique(array) {
30 | var a = [];
31 | for (var i=0, l=array.length; i": function(a, b) {
54 | return a > b;
55 | },
56 | ">=": function(a, b) {
57 | return a >= b;
58 | },
59 | "<": function(a, b, c) {
60 | return (c === undefined) ? a < b : (a < b) && (b < c);
61 | },
62 | "<=": function(a, b, c) {
63 | return (c === undefined) ? a <= b : (a <= b) && (b <= c);
64 | },
65 | "!!": function(a) {
66 | return jsonLogic.truthy(a);
67 | },
68 | "!": function(a) {
69 | return !jsonLogic.truthy(a);
70 | },
71 | "%": function(a, b) {
72 | return a % b;
73 | },
74 | "log": function(a) {
75 | console.log(a); return a;
76 | },
77 | "in": function(a, b) {
78 | if(!b || typeof b.indexOf === "undefined") return false;
79 | return (b.indexOf(a) !== -1);
80 | },
81 | "cat": function() {
82 | return Array.prototype.join.call(arguments, "");
83 | },
84 | "substr":function(source, start, end) {
85 | if(end < 0){
86 | // JavaScript doesn't support negative end, this emulates PHP behavior
87 | var temp = String(source).substr(start);
88 | return temp.substr(0, temp.length + end);
89 | }
90 | return String(source).substr(start, end);
91 | },
92 | "+": function() {
93 | return Array.prototype.reduce.call(arguments, function(a, b) {
94 | return parseFloat(a, 10) + parseFloat(b, 10);
95 | }, 0);
96 | },
97 | "*": function() {
98 | return Array.prototype.reduce.call(arguments, function(a, b) {
99 | return parseFloat(a, 10) * parseFloat(b, 10);
100 | });
101 | },
102 | "-": function(a, b) {
103 | if(b === undefined) {
104 | return -a;
105 | }else{
106 | return a - b;
107 | }
108 | },
109 | "/": function(a, b) {
110 | return a / b;
111 | },
112 | "min": function() {
113 | return Math.min.apply(this, arguments);
114 | },
115 | "max": function() {
116 | return Math.max.apply(this, arguments);
117 | },
118 | "merge": function() {
119 | return Array.prototype.reduce.call(arguments, function(a, b) {
120 | return a.concat(b);
121 | }, []);
122 | },
123 | "var": function(a, b) {
124 | var not_found = (b === undefined) ? null : b;
125 | var data = this;
126 | if(typeof a === "undefined" || a==="" || a===null) {
127 | return data;
128 | }
129 | var sub_props = String(a).split(".");
130 | for(var i = 0; i < sub_props.length; i++) {
131 | if(data === null) {
132 | return not_found;
133 | }
134 | // Descending into data
135 | data = data[sub_props[i]];
136 | if(data === undefined) {
137 | return not_found;
138 | }
139 | }
140 | return data;
141 | },
142 | "missing": function() {
143 | /*
144 | Missing can receive many keys as many arguments, like {"missing:[1,2]}
145 | Missing can also receive *one* argument that is an array of keys,
146 | which typically happens if it's actually acting on the output of another command
147 | (like 'if' or 'merge')
148 | */
149 |
150 | var missing = [];
151 | var keys = Array.isArray(arguments[0]) ? arguments[0] : arguments;
152 |
153 | for(var i = 0; i < keys.length; i++) {
154 | var key = keys[i];
155 | var value = jsonLogic.apply({"var": key}, this);
156 | if(value === null || value === "") {
157 | missing.push(key);
158 | }
159 | }
160 |
161 | return missing;
162 | },
163 | "missing_some": function(need_count, options) {
164 | // missing_some takes two arguments, how many (minimum) items must be present, and an array of keys (just like 'missing') to check for presence.
165 | var are_missing = jsonLogic.apply({"missing": options}, this);
166 |
167 | if(options.length - are_missing.length >= need_count) {
168 | return [];
169 | }else{
170 | return are_missing;
171 | }
172 | },
173 | "method": function(obj, method, args) {
174 | return obj[method].apply(obj, args);
175 | },
176 |
177 | };
178 |
179 | jsonLogic.is_logic = function(logic) {
180 | return (
181 | typeof logic === "object" && // An object
182 | logic !== null && // but not null
183 | ! Array.isArray(logic) && // and not an array
184 | Object.keys(logic).length === 1 // with exactly one key
185 | );
186 | };
187 |
188 | /*
189 | This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives. E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer.
190 |
191 | Spec and rationale here: http://jsonlogic.com/truthy
192 | */
193 | jsonLogic.truthy = function(value) {
194 | if(Array.isArray(value) && value.length === 0) {
195 | return false;
196 | }
197 | return !! value;
198 | };
199 |
200 |
201 | jsonLogic.get_operator = function(logic) {
202 | return Object.keys(logic)[0];
203 | };
204 |
205 | jsonLogic.get_values = function(logic) {
206 | return logic[jsonLogic.get_operator(logic)];
207 | };
208 |
209 | jsonLogic.apply = function(logic, data) {
210 | // Does this array contain logic? Only one way to find out.
211 | if(Array.isArray(logic)) {
212 | return logic.map(function(l) {
213 | return jsonLogic.apply(l, data);
214 | });
215 | }
216 | // You've recursed to a primitive, stop!
217 | if( ! jsonLogic.is_logic(logic) ) {
218 | return logic;
219 | }
220 |
221 | data = data || {};
222 |
223 | var op = jsonLogic.get_operator(logic);
224 | var values = logic[op];
225 | var i;
226 | var current;
227 | var scopedLogic, scopedData, filtered, initial;
228 |
229 | // easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]}
230 | if( ! Array.isArray(values)) {
231 | values = [values];
232 | }
233 |
234 | // 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed.
235 | if(op === "if" || op == "?:") {
236 | /* 'if' should be called with a odd number of parameters, 3 or greater
237 | This works on the pattern:
238 | if( 0 ){ 1 }else{ 2 };
239 | if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 };
240 | if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 };
241 |
242 | The implementation is:
243 | For pairs of values (0,1 then 2,3 then 4,5 etc)
244 | If the first evaluates truthy, evaluate and return the second
245 | If the first evaluates falsy, jump to the next pair (e.g, 0,1 to 2,3)
246 | given one parameter, evaluate and return it. (it's an Else and all the If/ElseIf were false)
247 | given 0 parameters, return NULL (not great practice, but there was no Else)
248 | */
249 | for(i = 0; i < values.length - 1; i += 2) {
250 | if( jsonLogic.truthy( jsonLogic.apply(values[i], data) ) ) {
251 | return jsonLogic.apply(values[i+1], data);
252 | }
253 | }
254 | if(values.length === i+1) return jsonLogic.apply(values[i], data);
255 | return null;
256 | }else if(op === "and") { // Return first falsy, or last
257 | for(i=0; i < values.length; i+=1) {
258 | current = jsonLogic.apply(values[i], data);
259 | if( ! jsonLogic.truthy(current)) {
260 | return current;
261 | }
262 | }
263 | return current; // Last
264 | }else if(op === "or") {// Return first truthy, or last
265 | for(i=0; i < values.length; i+=1) {
266 | current = jsonLogic.apply(values[i], data);
267 | if( jsonLogic.truthy(current) ) {
268 | return current;
269 | }
270 | }
271 | return current; // Last
272 |
273 |
274 |
275 |
276 | }else if(op === 'filter'){
277 | scopedData = jsonLogic.apply(values[0], data);
278 | scopedLogic = values[1];
279 |
280 | if ( ! Array.isArray(scopedData)) {
281 | return [];
282 | }
283 | // Return only the elements from the array in the first argument,
284 | // that return truthy when passed to the logic in the second argument.
285 | // For parity with JavaScript, reindex the returned array
286 | return scopedData.filter(function(datum){
287 | return jsonLogic.truthy( jsonLogic.apply(scopedLogic, datum));
288 | });
289 | }else if(op === 'map'){
290 | scopedData = jsonLogic.apply(values[0], data);
291 | scopedLogic = values[1];
292 |
293 | if ( ! Array.isArray(scopedData)) {
294 | return [];
295 | }
296 |
297 | return scopedData.map(function(datum){
298 | return jsonLogic.apply(scopedLogic, datum);
299 | });
300 |
301 | }else if(op === 'reduce'){
302 | scopedData = jsonLogic.apply(values[0], data);
303 | scopedLogic = values[1];
304 | initial = typeof values[2] !== 'undefined' ? values[2] : null;
305 |
306 | if ( ! Array.isArray(scopedData)) {
307 | return initial;
308 | }
309 |
310 | return scopedData.reduce(
311 | function(accumulator, current){
312 | return jsonLogic.apply(
313 | scopedLogic,
314 | {'current':current, 'accumulator':accumulator}
315 | );
316 | },
317 | initial
318 | );
319 |
320 | }else if(op === "all") {
321 | scopedData = jsonLogic.apply(values[0], data);
322 | scopedLogic = values[1];
323 | // All of an empty set is false. Note, some and none have correct fallback after the for loop
324 | if( ! scopedData.length) {
325 | return false;
326 | }
327 | for(i=0; i < scopedData.length; i+=1) {
328 | if( ! jsonLogic.truthy( jsonLogic.apply(scopedLogic, scopedData[i]) )) {
329 | return false; // First falsy, short circuit
330 | }
331 | }
332 | return true; // All were truthy
333 | }else if(op === "none") {
334 | filtered = jsonLogic.apply({'filter' : values}, data);
335 | return filtered.length === 0;
336 |
337 | }else if(op === "some") {
338 | filtered = jsonLogic.apply({'filter' : values}, data);
339 | return filtered.length > 0;
340 | }
341 |
342 | // Everyone else gets immediate depth-first recursion
343 | values = values.map(function(val) {
344 | return jsonLogic.apply(val, data);
345 | });
346 |
347 |
348 | // The operation is called with "data" bound to its "this" and "values" passed as arguments.
349 | // Structured commands like % or > can name formal arguments while flexible commands (like missing or merge) can operate on the pseudo-array arguments
350 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments
351 | if(typeof operations[op] === "function") {
352 | return operations[op].apply(data, values);
353 | }else if(op.indexOf(".") > 0) { // Contains a dot, and not in the 0th position
354 | var sub_ops = String(op).split(".");
355 | var operation = operations;
356 | for(i = 0; i < sub_ops.length; i++) {
357 | // Descending into operations
358 | operation = operation[sub_ops[i]];
359 | if(operation === undefined) {
360 | throw new Error("Unrecognized operation " + op +
361 | " (failed at " + sub_ops.slice(0, i+1).join(".") + ")");
362 | }
363 | }
364 |
365 | return operation.apply(data, values);
366 | }
367 |
368 | throw new Error("Unrecognized operation " + op );
369 | };
370 |
371 | jsonLogic.uses_data = function(logic) {
372 | var collection = [];
373 |
374 | if( jsonLogic.is_logic(logic) ) {
375 | var op = jsonLogic.get_operator(logic);
376 | var values = logic[op];
377 |
378 | if( ! Array.isArray(values)) {
379 | values = [values];
380 | }
381 |
382 | if(op === "var") {
383 | // This doesn't cover the case where the arg to var is itself a rule.
384 | collection.push(values[0]);
385 | }else{
386 | // Recursion!
387 | values.map(function(val) {
388 | collection.push.apply(collection, jsonLogic.uses_data(val) );
389 | });
390 | }
391 | }
392 |
393 | return arrayUnique(collection);
394 | };
395 |
396 | jsonLogic.add_operation = function(name, code) {
397 | operations[name] = code;
398 | };
399 |
400 | jsonLogic.rm_operation = function(name) {
401 | delete operations[name];
402 | };
403 |
404 | jsonLogic.rule_like = function(rule, pattern) {
405 | // console.log("Is ". JSON.stringify(rule) . " like " . JSON.stringify(pattern) . "?");
406 | if(pattern === rule) {
407 | return true;
408 | } // TODO : Deep object equivalency?
409 | if(pattern === "@") {
410 | return true;
411 | } // Wildcard!
412 | if(pattern === "number") {
413 | return (typeof rule === "number");
414 | }
415 | if(pattern === "string") {
416 | return (typeof rule === "string");
417 | }
418 | if(pattern === "array") {
419 | // !logic test might be superfluous in JavaScript
420 | return Array.isArray(rule) && ! jsonLogic.is_logic(rule);
421 | }
422 |
423 | if(jsonLogic.is_logic(pattern)) {
424 | if(jsonLogic.is_logic(rule)) {
425 | var pattern_op = jsonLogic.get_operator(pattern);
426 | var rule_op = jsonLogic.get_operator(rule);
427 |
428 | if(pattern_op === "@" || pattern_op === rule_op) {
429 | // echo "\nOperators match, go deeper\n";
430 | return jsonLogic.rule_like(
431 | jsonLogic.get_values(rule, false),
432 | jsonLogic.get_values(pattern, false)
433 | );
434 | }
435 | }
436 | return false; // pattern is logic, rule isn't, can't be eq
437 | }
438 |
439 | if(Array.isArray(pattern)) {
440 | if(Array.isArray(rule)) {
441 | if(pattern.length !== rule.length) {
442 | return false;
443 | }
444 | /*
445 | Note, array order MATTERS, because we're using this array test logic to consider arguments, where order can matter. (e.g., + is commutative, but '-' or 'if' or 'var' are NOT)
446 | */
447 | for(var i = 0; i < pattern.length; i += 1) {
448 | // If any fail, we fail
449 | if( ! jsonLogic.rule_like(rule[i], pattern[i])) {
450 | return false;
451 | }
452 | }
453 | return true; // If they *all* passed, we pass
454 | }else{
455 | return false; // Pattern is array, rule isn't
456 | }
457 | }
458 |
459 | // Not logic, not array, not a === match for rule.
460 | return false;
461 | };
462 |
463 | return jsonLogic;
464 | }));
465 |
--------------------------------------------------------------------------------
/legislation.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BetterRules/example-rules-as-code/c0ef52c936c754086421a3d659b03f9c69079830/legislation.pdf
--------------------------------------------------------------------------------
/python-authorityspoke/.gitignore:
--------------------------------------------------------------------------------
1 | *.pytest_cache
2 | *.ipynb_checkpoints
3 | *__pycache__/
4 | venv/
--------------------------------------------------------------------------------
/python-authorityspoke/README.md:
--------------------------------------------------------------------------------
1 | # Rules as Code using Python with AuthoritySpoke
2 |
3 | ## Installation
4 |
5 | To install [AuthoritySpoke](https://github.com/mscarey/AuthoritySpoke), use:
6 |
7 | pip install authorityspoke
8 |
9 | You can then import authorityspoke in python.
10 |
11 | ## Viewing the Notebook
12 |
13 | To install JupyterLab and use it to view the code examples with explanations, use:
14 |
15 | pip install jupyterlab
16 | jupyter lab
17 |
18 | and then open `statute_rules.ipynb`, which is in the same folder as this readme.
19 |
20 | ## Running the Tests
21 |
22 | To run the test suite, install and run pytest.
23 |
24 | pip install pytest
25 | pytest
26 |
27 | For more information and other tutorials, see the [AuthoritySpoke documentation](https://authorityspoke.readthedocs.io/en/latest/).
28 |
--------------------------------------------------------------------------------
/python-authorityspoke/conftest.py:
--------------------------------------------------------------------------------
1 | """Pytest fixtures for AuthoritySpoke "Beard Tax Act" examples"""
2 |
3 | from typing import Dict, List
4 |
5 | import pytest
6 |
7 | from legislice.mock_clients import MOCK_BEARD_ACT_CLIENT
8 |
9 | from authorityspoke.io import loaders, readers
10 |
11 | from authorityspoke.rules import Rule
12 |
13 | client = MOCK_BEARD_ACT_CLIENT
14 |
15 |
16 | @pytest.fixture(scope="function")
17 | def make_beard_rule() -> List[Rule]:
18 | """Rules from the "Beard Tax Act" example statutes."""
19 | beard_dictionary = loaders.load_holdings("beard_rules.json")
20 | return readers.read_holdings(beard_dictionary, client=client)
21 |
--------------------------------------------------------------------------------
/python-authorityspoke/example_data/codes/beard_tax_act.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | Australian Beard Tax (Promotion of Enlightenment Values) Act 1934
9 |
10 |
11 |
12 |
13 | Part 1—Preliminary
14 |
15 | 1
16 | Short title
17 | This Act may be cited as the Australian Beard Tax (Promotion of Enlightenment Values) Act 1934.
18 |
19 |
20 | 2
21 | Commencement
22 | This Act shall commence on 1 April 1935.
23 |
24 |
25 | 3
26 | Purpose
27 | This Act is enacted for the purpose of the promotion of enlightenment values and to discourage and dissuade the wearing of beards within the Commonwealth of Australia.
28 |
29 |
30 | 4
31 | Beard, defined
32 | In this Act, beard means any facial hair no shorter than 5 millimetres in length that:
33 |
34 | (a)
35 | occurs on or below the chin, or
36 |
37 |
38 | (b)
39 | exists in an uninterrupted line from the front of one ear to the front of the other ear below the nose.
40 |
41 |
42 |
43 |
44 | Part 2—Prohibition of beards
45 |
46 | 5
47 | Prohibition of beards
48 | The wearing of any beard whatsoever, except as provided in section 6, is prohibited within the Commonwealth of Australia.
49 |
50 |
51 | 6
52 | Exemption
53 |
54 | (1)
55 | The office of the Department of Beards may, from time to time or as they see fit, grant exemptions to persons from the prohibition contained in section 5.
56 |
57 |
58 | (2)
59 | Any such exemption granted under subsection 1 is to be for no longer than a period of 12 months.
60 |
61 |
62 |
63 |
64 | Part 2A—Beard tax
65 |
66 | 6A
67 | Levy of beard tax
68 | Where the Department provides an exemption from the prohibition in section 5, except as defined in section 6D, the person to whom such exemption is granted shall be liable to pay to the Department of Beards such fee as may be levied under section 6B.
69 |
70 |
71 | 6B
72 | Regulatory power of the Minister for Beards
73 | The Minister for Beards may, by Order in Council, issue such regulations, including but not limited to levies to be paid by persons exempted under section 6 from the prohibition in section 5, as is necessary for the good governance and financial stability of the Department of Beards.
74 |
75 |
76 | 6C
77 | Issuance of beardcoin
78 | Where an exemption is granted under section 6, the Department of Beards shall issue to the person so exempted a token, hereinafter referred to as a beardcoin, that shall for all purposes be regarded as substantive proof of such exemption.
79 |
80 |
81 | 6D
82 | Waiver of beard tax in special circumstances
83 |
84 | (1)
85 | The Department of Beards shall waive the collection of beard tax upon issuance of beardcoin under Section 6C where the reason the maintainer wears a beard is due to bona fide religious or cultural reasons.
86 |
87 |
88 | (2)
89 | The determination of the Department of Beards as to what constitutes bona fide religious or cultural reasons shall be final and no right of appeal shall exist.
90 |
91 |
92 |
93 |
94 | Part 3—Offences
95 |
96 | Wearing of a beard without exemption
97 | Any person found to be wearing a beard within the Commonwealth of Australia without proper exemption as granted under section 6 commits an offence.
98 |
99 |
100 | Improper transfer of beardcoin
101 | It shall be an offence to buy, sell, lend, lease, gift, transfer or receive in any way a beardcoin from any person or body other than the Department of Beards, except as provided in Part 4.
102 |
103 |
104 | Counterfeit beardcoin
105 |
106 | (1)
107 | It shall be an offense to produce, alter, or manufacture tokens with the appearance of and purporting to be genuine beardcoin.
108 |
109 |
110 | (2)
111 | It shall be no defense to a charge under section 7A that the purchase, sale, lease, gift, transfer or receipt was of counterfeit beardcoin rather than genuine beardcoin.
112 |
113 |
114 |
115 | Notice to remedy
116 |
117 | (1)
118 | Where an officer of the Department of Beards, Australian Federal Police, state or territorial police, or military police of the Australian Defence Force finds a person to be wearing a beard within the territory of the Commonwealth of Australia, and that person fails or is unable to produce a beardcoin as proof of holding an exemption under section 6, that officer shall in the first instance issue such person a notice to remedy
119 |
120 |
121 | (2)
122 | Any such person issued a notice to remedy under subsection 1 must either:
123 |
124 | (a)
125 | shave in such a way that they are no longer in breach of section 5, or
126 |
127 |
128 | (b)
129 | obtain a beardcoin from the Department of Beards
130 |
131 | within 14 days of such notice being issued to them.
132 |
133 |
134 |
135 | Penalties
136 |
137 | (1)
138 | Any person summarily convicted of a first offence under section 7 of unlawfully wearing a beard within the Commonwealth of Australia shall be liable to a fine not exceeding $200.
139 |
140 |
141 | (2)
142 | Any person summarily convicted of a second or subsequent offence under section 7 shall be liable to a fine not exceeding $1000, or a period of imprisonment until such time as they no longer are in breach of section 5.
143 |
144 |
145 | (3)
146 | No penalty shall be applied to any person who, within 14 days of receiving a notice to remedy under section 8(1), takes the action required of them under section 8(2).
147 |
148 |
149 | (4)
150 | Any person convicted of an offence under section 7A or section 7B(1) shall be liable to a fine not exceeding $5000 and/or a period of imprisonment not exceeding 12 months.
151 |
152 |
153 |
154 |
155 |
156 | Purpose of Part 4
157 | Part 4 of the Australian Beard Tax (Promotion of Enlightement Values) Act 1934 exists for the purpose of incentivising Australians, and those visiting Australia, to relieve themselves from the burdensome habit of wearing a beard.
158 |
159 |
160 | Licensed repurchasers of beardcoin
161 | The Department of Beards may issue licenses to such barbers, hairdressers or other male grooming professionals as they see fit to purchase a beardcoin from a customer whose beard they have removed, and to resell those beardcoins to the Department of Beards
162 |
163 |
164 | Rate to be paid to repurchasers of beardcoin
165 | The value to be transfered to licensed repurchasers of beardcoin under section 11 shall be defined by Order in Council by the Minister for Beards under section 6B on a per coin basis.
166 |
167 |
168 |
169 | Part 5—Consequential amendments
170 |
171 | Removal of GST from razors and shavers
172 | The items set out in Annexe 1 of this Act shall be exempt from the goods and sales tax provisions of the Taxation Administration Act 1953.
173 |
174 |
175 |
176 |
177 |
--------------------------------------------------------------------------------
/python-authorityspoke/example_data/holdings/beard_rules.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "inputs": [
4 | {
5 | "type": "fact",
6 | "content": "{the suspected beard} was facial hair"
7 | },
8 | {
9 | "type": "fact",
10 | "content": "the length of the suspected beard was >= 5 millimetres"
11 | },
12 | {
13 | "type": "fact",
14 | "content": "the suspected beard occurred on or below the chin"
15 | }
16 | ],
17 | "outputs": [
18 | {
19 | "type": "fact",
20 | "content": "the suspected beard was a beard",
21 | "name": "the fact that the facial hair was a beard"
22 | }
23 | ],
24 | "enactments": [
25 | {
26 | "node": "/test/acts/47/4",
27 | "exact": "In this Act, beard means any facial hair no shorter than 5 millimetres in length that: occurs on or below the chin"
28 | }
29 | ],
30 | "universal": true
31 | },
32 | {
33 | "inputs": [
34 | {
35 | "type": "fact",
36 | "content": "{the suspected beard} was facial hair"
37 | },
38 | {
39 | "type": "fact",
40 | "content": "the length of the suspected beard was >= 5 millimetres"
41 | },
42 | {
43 | "type": "fact",
44 | "content": "the suspected beard existed in an uninterrupted line from the front of one ear to the front of the other ear below the nose"
45 | }
46 | ],
47 | "outputs": [
48 | {
49 | "type": "fact",
50 | "content": "the suspected beard was a beard",
51 | "name": "the fact that the facial hair was a beard"
52 | }
53 | ],
54 | "enactments": [
55 | {
56 | "node": "/test/acts/47/4",
57 | "selection": [
58 | {
59 | "exact": "In this Act, beard means any facial hair no shorter than 5 millimetres in length that:"
60 | },
61 | {
62 | "exact": "exists in an uninterrupted line from the front of one ear to the front of the other ear below the nose."
63 | }
64 | ]
65 | },
66 | {
67 | "node": "/test/acts/47/4/b"
68 | }
69 | ],
70 | "universal": true
71 | },
72 | {
73 | "inputs": [
74 | "the fact that the facial hair was a beard",
75 | {
76 | "type": "fact",
77 | "content": "{the defendant} wore the suspected beard",
78 | "name": "the defendant's act of wearing the suspected beard"
79 | },
80 | {
81 | "type": "fact",
82 | "content": "the office of {the Department of Beards} granted an exemption authorizing the defendant's act of wearing the suspected beard",
83 | "truth": false
84 | }
85 | ],
86 | "outputs": {
87 | "type": "fact",
88 | "content": "{the defendant} committed the offense of wearing of a beard without exemption",
89 | "name": "offense of wearing a beard without exemption"
90 | },
91 | "enactments": [
92 | {
93 | "node": "/test/acts/47/5"
94 | },
95 | {
96 | "node": "/test/acts/47/7"
97 | }
98 | ],
99 | "enactments_despite": {
100 | "node": "/test/acts/47/6"
101 | },
102 | "universal": true
103 | },
104 | {
105 | "inputs": {
106 | "type": "fact",
107 | "content": "the Department of Beards granted an exemption authorizing the defendant's act of wearing the suspected beard",
108 | "name": "the Department of Beards granted the defendant's beard exemption"
109 | },
110 | "outputs": {
111 | "type": "fact",
112 | "content": "the Department of Beards granted an exemption from the prohibition of wearing beards"
113 | },
114 | "universal": true,
115 | "mandatory": true,
116 | "enactments": {
117 | "node": "/test/acts/47/6/1"
118 | }
119 | },
120 | {
121 | "inputs": {
122 | "type": "exhibit",
123 | "form": "token",
124 | "statement": "the Department of Beards granted the defendant's beard exemption",
125 | "statement_attribution": "the Department of Beards",
126 | "name": "the defendant's beardcoin"
127 | },
128 | "outputs": {
129 | "type": "evidence",
130 | "exhibit": "the defendant's beardcoin",
131 | "to_effect": "the Department of Beards granted the defendant's beard exemption"
132 | },
133 | "universal": true,
134 | "enactments": [
135 | {
136 | "node": "/test/acts/47/6C"
137 | }
138 | ]
139 | },
140 | {
141 | "inputs": {
142 | "type": "fact",
143 | "content": "{the beardcoin transaction} was {the defendant}'s purchase of any beardcoin from {the counterparty}",
144 | "context_factors": [
145 | {
146 | "type": "exhibit",
147 | "form": "token",
148 | "statement": {
149 | "type": "fact",
150 | "content": "the Department of Beards granted an exemption from the prohibition of wearing beards",
151 | "name": "the Department of Beards granted a beard exemption"
152 | },
153 | "statement_attribution": "the Department of Beards",
154 | "name": "any beardcoin"
155 | }
156 | ]
157 | },
158 | "outputs": {
159 | "type": "fact",
160 | "content": "the beardcoin transaction was a transfer of beardcoin between the defendant and the counterparty",
161 | "name": "beardcoin transfer"
162 | },
163 | "enactments": {
164 | "node": "/test/acts/47/7A"
165 | },
166 | "universal": true,
167 | "mandatory": true
168 | },
169 | {
170 | "inputs": {
171 | "type": "fact",
172 | "content": "the beardcoin transaction was the counterparty's purchase of any beardcoin from the defendant"
173 | },
174 | "outputs": "beardcoin transfer",
175 | "enactments": {
176 | "node": "/test/acts/47/7A"
177 | },
178 | "universal": true,
179 | "mandatory": true
180 | },
181 | {
182 | "inputs": {
183 | "type": "fact",
184 | "content": "the beardcoin transaction was the defendant's loan of any beardcoin to the counterparty"
185 | },
186 | "outputs": "beardcoin transfer",
187 | "enactments": {
188 | "node": "/test/acts/47/7A"
189 | },
190 | "universal": true,
191 | "mandatory": true
192 | },
193 | {
194 | "inputs": {
195 | "type": "fact",
196 | "content": "the beardcoin transaction was the defendant's lease of any beardcoin to the counterparty"
197 | },
198 | "outputs": "beardcoin transfer",
199 | "enactments": {
200 | "node": "/test/acts/47/7A"
201 | },
202 | "universal": true,
203 | "mandatory": true
204 | },
205 | {
206 | "inputs": {
207 | "type": "fact",
208 | "content": "the beardcoin transaction was the defendant's gift of any beardcoin to the counterparty"
209 | },
210 | "outputs": "beardcoin transfer",
211 | "enactments": {
212 | "node": "/test/acts/47/7A"
213 | },
214 | "universal": true,
215 | "mandatory": true
216 | },
217 | {
218 | "inputs": {
219 | "type": "fact",
220 | "content": "the beardcoin transaction was the defendant's receipt of any beardcoin from the counterparty"
221 | },
222 | "outputs": "beardcoin transfer",
223 | "enactments": {
224 | "node": "/test/acts/47/7A"
225 | },
226 | "universal": true,
227 | "mandatory": true
228 | },
229 | {
230 | "inputs": [
231 | "beardcoin transfer",
232 | {
233 | "type": "fact",
234 | "content": "the beardcoin transaction was a licensed beardcoin repurchase",
235 | "absent": true
236 | },
237 | {
238 | "type": "fact",
239 | "content": "the counterparty was the Department of Beards",
240 | "truth": false
241 | }
242 | ],
243 | "despite": {
244 | "type": "fact",
245 | "content": "any beardcoin was counterfeit"
246 | },
247 | "outputs": {
248 | "type": "fact",
249 | "content": "the defendant committed the offense of improper transfer of beardcoin"
250 | },
251 | "enactments": [
252 | {
253 | "node": "/test/acts/47/7A"
254 | },
255 | {
256 | "node": "/test/acts/47/7B/2"
257 | }
258 | ],
259 | "enactments_despite": [
260 | {
261 | "node": "/test/acts/47/11"
262 | }
263 | ],
264 | "mandatory": true,
265 | "universal": true
266 | },
267 | {
268 | "inputs": {
269 | "type": "fact",
270 | "content": "the defendant produced, altered, or manufactured tokens with the appearance of and purporting to be genuine beardcoin"
271 | },
272 | "outputs": {
273 | "type": "fact",
274 | "content": "the defendant committed the offense of counterfeiting beardcoin"
275 | },
276 | "enactments": [
277 | {
278 | "node": "/test/acts/47/7B/1"
279 | }
280 | ]
281 | },
282 | {
283 | "inputs": [
284 | {
285 | "type": "fact",
286 | "content": "the beardcoin transaction was {the barber}'s purchase of any beardcoin from {the customer}"
287 | },
288 | {
289 | "type": "fact",
290 | "content": "the barber removed the customer's beard with barbering, hairdressing, or other male grooming services"
291 | },
292 | {
293 | "type": "fact",
294 | "content": "the Department of Beards licensed the barber to purchase beardcoins from customers"
295 | }
296 | ],
297 | "outputs": {
298 | "type": "fact",
299 | "content": "the beardcoin transaction was a licensed beardcoin repurchase"
300 | },
301 | "enactments": {
302 | "node": "/test/acts/47/11"
303 | }
304 | }
305 | ]
--------------------------------------------------------------------------------
/python-authorityspoke/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | pytest
2 |
--------------------------------------------------------------------------------
/python-authorityspoke/requirements.txt:
--------------------------------------------------------------------------------
1 | AuthoritySpoke==0.4.0
2 | legislice==0.1.1
--------------------------------------------------------------------------------
/python-authorityspoke/statute_rules.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Legislative Rule Models with AuthoritySpoke"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "This tutorial will show how to use [AuthoritySpoke](https://authorityspoke.readthedocs.io/en/latest/) to model legal rules found in legislation. This is a departure from most of the AuthoritySpoke documentation, which focuses on judicial holdings.\n",
15 | "\n",
16 | "These examples are based on the fictional [Australian Beard Tax (Promotion of Enlightenment Values) Act 1934](https://github.com/ServiceInnovationLab/example-rules-as-code), which was created thanks to the New Zealand [Service Innovation Lab](https://github.com/ServiceInnovationLab), to supply test data for experimental legal rule automation systems."
17 | ]
18 | },
19 | {
20 | "cell_type": "markdown",
21 | "metadata": {},
22 | "source": [
23 | "The Service Innovation Lab's version of the Beard Tax Act is [a PDF](https://github.com/ServiceInnovationLab/example-rules-as-code/blob/master/legislation.pdf). AuthoritySpoke is designed to load legislation from JSON data using a related Python library called [Legislice](https://github.com/mscarey/legislice). So I've set up a web API that can serve the provisions of the Beard Act. You can also [browse the provisions](https://authorityspoke.com/legislice/test/) of the Beard Act in your web browser.\n",
24 | "\n",
25 | "For convenience, this tutorial will use Legislice's mock API server with fake JSON responses, instead of connecting to the real API."
26 | ]
27 | },
28 | {
29 | "cell_type": "code",
30 | "execution_count": 1,
31 | "metadata": {},
32 | "outputs": [],
33 | "source": [
34 | "from legislice.mock_clients import MOCK_BEARD_ACT_CLIENT\n",
35 | "client = MOCK_BEARD_ACT_CLIENT"
36 | ]
37 | },
38 | {
39 | "cell_type": "markdown",
40 | "metadata": {},
41 | "source": [
42 | "Next, I'll prepare annotations for the statute provisions in a JSON file, and then load them as a Python dictionary. AuthoritySpoke rules are procedural, so they have one or more outputs, and zero or more inputs. They can also have \"despite\" factors, which are factors that may not support the output, but they don't preclude the output either.\n",
43 | "\n",
44 | "AuthoritySpoke rules are usually supported by enactments such as statute sections. These can be retrieved from the API with the URI-like identifiers like those in United States Legislative Markup (USLM). In this case, since the Beard Tax Act is Act 47 of 1934, the identifier for Section 4 of the Act is [/test/acts/47/4](https://authorityspoke.com/legislice/test/acts/47/4@2035-08-01).\n",
45 | "\n",
46 | "In AuthoritySpoke, you have to think about two JSON schemas: there's one schema for legislative provisions fetched from the web API, and another schema for rule annotations that you (currently) have to create for yourself. Of course, you can create either type of object directly in Python instead of loading them from a JSON file. For details, see the [AuthoritySpoke reference manual](https://authorityspoke.readthedocs.io/en/latest/). Here's an example of one JSON rule annotation."
47 | ]
48 | },
49 | {
50 | "cell_type": "code",
51 | "execution_count": 2,
52 | "metadata": {},
53 | "outputs": [
54 | {
55 | "data": {
56 | "text/plain": [
57 | "{'inputs': [{'type': 'fact',\n",
58 | " 'content': '{the suspected beard} was facial hair'},\n",
59 | " {'type': 'fact',\n",
60 | " 'content': 'the length of the suspected beard was >= 5 millimetres'},\n",
61 | " {'type': 'fact',\n",
62 | " 'content': 'the suspected beard occurred on or below the chin'}],\n",
63 | " 'outputs': [{'type': 'fact',\n",
64 | " 'content': 'the suspected beard was a beard',\n",
65 | " 'name': 'the fact that the facial hair was a beard'}],\n",
66 | " 'enactments': [{'node': '/test/acts/47/4',\n",
67 | " 'exact': 'In this Act, beard means any facial hair no shorter than 5 millimetres in length that: occurs on or below the chin'}],\n",
68 | " 'universal': True}"
69 | ]
70 | },
71 | "execution_count": 2,
72 | "metadata": {},
73 | "output_type": "execute_result"
74 | }
75 | ],
76 | "source": [
77 | "from authorityspoke.io import loaders\n",
78 | "\n",
79 | "beard_dictionary = loaders.load_holdings(\"beard_rules.json\")\n",
80 | "beard_dictionary[0]"
81 | ]
82 | },
83 | {
84 | "cell_type": "markdown",
85 | "metadata": {},
86 | "source": [
87 | "The \"universal\" True/False field indicates whether the Rule is one that applies in every case where all of the inputs are present, or only in some cases. The default is False, but this Rule overrides that default and says it applies in every case where all of the inputs are present."
88 | ]
89 | },
90 | {
91 | "cell_type": "markdown",
92 | "metadata": {},
93 | "source": [
94 | "Now we can have AuthoritySpoke read this JSON and convert it to a list of Rule objects. In particular, we'll look at the first two Rules, which describe two ways that an object can be defined to be a \"beard\"."
95 | ]
96 | },
97 | {
98 | "cell_type": "code",
99 | "execution_count": 3,
100 | "metadata": {},
101 | "outputs": [
102 | {
103 | "name": "stdout",
104 | "output_type": "stream",
105 | "text": [
106 | "the Rule that the court MAY ALWAYS impose the\n",
107 | " RESULT:\n",
108 | " the Fact that was a beard\n",
109 | " GIVEN:\n",
110 | " the Fact that was facial hair\n",
111 | " the Fact that the length of was at least 5\n",
112 | " millimeter\n",
113 | " the Fact that occurred on or below the chin\n",
114 | " GIVEN the ENACTMENT:\n",
115 | " \"In this Act, beard means any facial hair no shorter than 5 millimetres in length that: occurs on or below the chin…\" (/test/acts/47/4 1935-04-01)\n"
116 | ]
117 | }
118 | ],
119 | "source": [
120 | "from authorityspoke.io import readers\n",
121 | "\n",
122 | "beard_rules = readers.read_rules(beard_dictionary, client=client)\n",
123 | "print(beard_rules[0])"
124 | ]
125 | },
126 | {
127 | "cell_type": "code",
128 | "execution_count": 4,
129 | "metadata": {},
130 | "outputs": [
131 | {
132 | "name": "stdout",
133 | "output_type": "stream",
134 | "text": [
135 | "the Rule that the court MAY ALWAYS impose the\n",
136 | " RESULT:\n",
137 | " the Fact that was a beard\n",
138 | " GIVEN:\n",
139 | " the Fact that was facial hair\n",
140 | " the Fact that the length of was at least 5\n",
141 | " millimeter\n",
142 | " the Fact that existed in an uninterrupted line\n",
143 | " from the front of one ear to the front of the other ear below the nose\n",
144 | " GIVEN the ENACTMENTS:\n",
145 | " \"In this Act, beard means any facial hair no shorter than 5 millimetres in length that:…exists in an uninterrupted line from the front of one ear to the front of the other ear below the nose.\" (/test/acts/47/4 1935-04-01)\n",
146 | " \"exists in an uninterrupted line from the front of one ear to the front of the other ear below the nose.\" (/test/acts/47/4/b 1935-04-01)\n"
147 | ]
148 | }
149 | ],
150 | "source": [
151 | "print(beard_rules[1])"
152 | ]
153 | },
154 | {
155 | "cell_type": "markdown",
156 | "metadata": {},
157 | "source": [
158 | "The difference between these two Rules is that the first one applies to facial hair \"on or below the chin\" and the second applies to facial hair \"in an uninterrupted line from the front of one ear to the front of the other ear below the nose\". I'll rename them accordingly."
159 | ]
160 | },
161 | {
162 | "cell_type": "code",
163 | "execution_count": 5,
164 | "metadata": {},
165 | "outputs": [],
166 | "source": [
167 | "chin_rule = beard_rules[0]\n",
168 | "ear_rule = beard_rules[1]"
169 | ]
170 | },
171 | {
172 | "cell_type": "markdown",
173 | "metadata": {},
174 | "source": [
175 | "## Implication and Contradiction between Rules\n",
176 | "\n",
177 | "AuthoritySpoke doesn't yet have a feature that directly takes a set of known Facts, applies a Rule to them, and then infers legal conclusions. Instead, in its current iteration, AuthoritySpoke can be used to combine Rules together to make more Rules, or to check whether Rules imply or contradict one another. \n",
178 | "\n",
179 | "For instance, if we create a new Rule that's identical to the first Rule in the Beard Tax Act except that it applies to facial hair that's exactly 8 millimeters long instead of \"no shorter than 5 millimetres\", we can determine that the original \"chin rule\" implies our new Rule."
180 | ]
181 | },
182 | {
183 | "cell_type": "code",
184 | "execution_count": 6,
185 | "metadata": {},
186 | "outputs": [
187 | {
188 | "name": "stdout",
189 | "output_type": "stream",
190 | "text": [
191 | "the Rule that the court MAY ALWAYS impose the\n",
192 | " RESULT:\n",
193 | " the Fact that was a beard\n",
194 | " GIVEN:\n",
195 | " the Fact that was facial hair\n",
196 | " the Fact that the length of was exactly equal to\n",
197 | " 8 millimeter\n",
198 | " the Fact that occurred on or below the chin\n",
199 | " GIVEN the ENACTMENT:\n",
200 | " \"In this Act, beard means any facial hair no shorter than 5 millimetres in length that: occurs on or below the chin…\" (/test/acts/47/4 1935-04-01)\n"
201 | ]
202 | }
203 | ],
204 | "source": [
205 | "beard_dictionary[0]['inputs'][1]['content'] = 'the length of the suspected beard was = 8 millimetres'\n",
206 | "longer_hair_rule = readers.read_rule(beard_dictionary[0], client=client)\n",
207 | "print(longer_hair_rule)"
208 | ]
209 | },
210 | {
211 | "cell_type": "code",
212 | "execution_count": 7,
213 | "metadata": {},
214 | "outputs": [
215 | {
216 | "data": {
217 | "text/plain": [
218 | "True"
219 | ]
220 | },
221 | "execution_count": 7,
222 | "metadata": {},
223 | "output_type": "execute_result"
224 | }
225 | ],
226 | "source": [
227 | "chin_rule.implies(longer_hair_rule)"
228 | ]
229 | },
230 | {
231 | "cell_type": "markdown",
232 | "metadata": {},
233 | "source": [
234 | "Similarly, we can create a new Rule that says facial hair is *never* a beard if its length is greater than 12 inches (we'll use inches instead of millimeters this time, and the units will be converted automatically thanks to the [pint](https://pint.readthedocs.io/en/stable/) library). And we can show that this new Rule contradicts a Rule that came from the Beard Tax Act."
235 | ]
236 | },
237 | {
238 | "cell_type": "code",
239 | "execution_count": 8,
240 | "metadata": {},
241 | "outputs": [
242 | {
243 | "name": "stdout",
244 | "output_type": "stream",
245 | "text": [
246 | "the Rule that the court MUST ALWAYS impose the\n",
247 | " RESULT:\n",
248 | " the Fact it is false that was a beard\n",
249 | " GIVEN:\n",
250 | " the Fact that the length of was at least 12 inch\n",
251 | " DESPITE:\n",
252 | " the Fact that was facial hair\n",
253 | " GIVEN the ENACTMENTS:\n",
254 | " \"In this Act, beard means any facial hair no shorter than 5 millimetres in length that:…exists in an uninterrupted line from the front of one ear to the front of the other ear below the nose.\" (/test/acts/47/4 1935-04-01)\n",
255 | " \"exists in an uninterrupted line from the front of one ear to the front of the other ear below the nose.\" (/test/acts/47/4/b 1935-04-01)\n"
256 | ]
257 | }
258 | ],
259 | "source": [
260 | "beard_dictionary[1][\"despite\"] = beard_dictionary[1][\"inputs\"][0]\n",
261 | "beard_dictionary[1][\"inputs\"] = {\n",
262 | " \"type\": \"fact\",\n",
263 | " \"content\": \"the length of the suspected beard was >= 12 inches\",\n",
264 | "}\n",
265 | "beard_dictionary[1][\"outputs\"][0][\"truth\"] = False\n",
266 | "beard_dictionary[1][\"mandatory\"] = True\n",
267 | "\n",
268 | "long_thing_is_not_a_beard = readers.read_rule(beard_dictionary[1], client=client)\n",
269 | "print(long_thing_is_not_a_beard)"
270 | ]
271 | },
272 | {
273 | "cell_type": "code",
274 | "execution_count": 9,
275 | "metadata": {},
276 | "outputs": [
277 | {
278 | "data": {
279 | "text/plain": [
280 | "True"
281 | ]
282 | },
283 | "execution_count": 9,
284 | "metadata": {},
285 | "output_type": "execute_result"
286 | }
287 | ],
288 | "source": [
289 | "long_thing_is_not_a_beard.contradicts(ear_rule)"
290 | ]
291 | },
292 | {
293 | "cell_type": "markdown",
294 | "metadata": {},
295 | "source": [
296 | "## Addition between Rules\n",
297 | "\n",
298 | "Finally, let's look at adding Rules. AuthoritySpoke currently only allows Rules to be added if applying the first Rule would supply you with all the input Factor you need to apply the second Rule as well. Here's an example.\n",
299 | "\n",
300 | "The Beard Tax Act defines the offense of \"improper transfer of beardcoin\". This offense basically has three elements:\n",
301 | "\n",
302 | "1. a transfer of beardcoin\n",
303 | "2. the absence of a license, and \n",
304 | "3. a counterparty who is not the Department of Beards.\n",
305 | "\n",
306 | "But in [section 7A](https://authorityspoke.com/legislice/test/acts/47/7A@2035-08-01) of the Beard Tax Act, we also learn specifically that a \"loan\" of the tokens called beardcoin counts as the kind of \"transfer\" that will support a conviction of the offense. We can represent this information as a separate Rule, and then add it to the Rule defining the offense. The result is that we discover an alternate way of establishing the offense:\n",
307 | "\n",
308 | "1. a loan of beardcoin\n",
309 | "2. the absence of a license, and \n",
310 | "3. a counterparty who is not the Department of Beards.\n",
311 | "\n",
312 | "Here are the two Rules we'll be adding together."
313 | ]
314 | },
315 | {
316 | "cell_type": "code",
317 | "execution_count": 10,
318 | "metadata": {},
319 | "outputs": [
320 | {
321 | "name": "stdout",
322 | "output_type": "stream",
323 | "text": [
324 | "the Rule that the court MUST ALWAYS impose the\n",
325 | " RESULT:\n",
326 | " the Fact that committed the offense of improper\n",
327 | " transfer of beardcoin\n",
328 | " GIVEN:\n",
329 | " the Fact that was a transfer of beardcoin\n",
330 | " between and \n",
331 | " absence of the Fact that was a licensed\n",
332 | " beardcoin repurchase\n",
333 | " the Fact it is false that was \n",
335 | " DESPITE:\n",
336 | " the Fact that the token attributed to ,\n",
337 | " asserting the fact that granted an\n",
338 | " exemption from the prohibition of wearing beards, was counterfeit\n",
339 | " GIVEN the ENACTMENTS:\n",
340 | " \"It shall be an offence to buy, sell, lend, lease, gift, transfer or receive in any way a beardcoin from any person or body other than the Department of Beards, except as provided in Part 4.\" (/test/acts/47/7A 1935-04-01)\n",
341 | " \"It shall be no defense to a charge under section 7A that the purchase, sale, lease, gift, transfer or receipt was of counterfeit beardcoin rather than genuine beardcoin.\" (/test/acts/47/7B/2 1935-04-01)\n",
342 | " DESPITE the ENACTMENT:\n",
343 | " \"The Department of Beards may issue licenses to such barbers, hairdressers, or other male grooming professionals as they see fit to purchase a beardcoin from a customer whose beard they have removed, and to resell those beardcoins to the Department of Beards.\" (/test/acts/47/11 2013-07-18)\n"
344 | ]
345 | }
346 | ],
347 | "source": [
348 | "elements_of_offense = beard_rules[11]\n",
349 | "print(elements_of_offense)"
350 | ]
351 | },
352 | {
353 | "cell_type": "code",
354 | "execution_count": 11,
355 | "metadata": {},
356 | "outputs": [
357 | {
358 | "name": "stdout",
359 | "output_type": "stream",
360 | "text": [
361 | "the Rule that the court MUST ALWAYS impose the\n",
362 | " RESULT:\n",
363 | " the Fact that was a transfer of beardcoin\n",
364 | " between and \n",
365 | " GIVEN:\n",
366 | " the Fact that was 's loan\n",
367 | " of the token attributed to , asserting the\n",
368 | " fact that granted an exemption from the\n",
369 | " prohibition of wearing beards, to \n",
370 | " GIVEN the ENACTMENT:\n",
371 | " \"It shall be an offence to buy, sell, lend, lease, gift, transfer or receive in any way a beardcoin from any person or body other than the Department of Beards, except as provided in Part 4.\" (/test/acts/47/7A 1935-04-01)\n"
372 | ]
373 | }
374 | ],
375 | "source": [
376 | "loan_is_transfer = beard_rules[7]\n",
377 | "print(loan_is_transfer)"
378 | ]
379 | },
380 | {
381 | "cell_type": "markdown",
382 | "metadata": {},
383 | "source": [
384 | "But there's a problem. The `loan_is_transfer` Rule establishes only one of the elements of the offense. In order to create a Rule that we can add to `elements_of_offense`, we'll need to add Facts establishing the two elements other than the \"transfer\" element. We'll also need to add one of the Enactments that the `elements_of_offense` Rule relies upon."
385 | ]
386 | },
387 | {
388 | "cell_type": "code",
389 | "execution_count": 12,
390 | "metadata": {},
391 | "outputs": [
392 | {
393 | "name": "stdout",
394 | "output_type": "stream",
395 | "text": [
396 | "the Rule that the court MUST ALWAYS impose the\n",
397 | " RESULT:\n",
398 | " the Fact that was a transfer of beardcoin\n",
399 | " between and \n",
400 | " GIVEN:\n",
401 | " the Fact that was 's loan\n",
402 | " of the token attributed to , asserting the\n",
403 | " fact that granted an exemption from the\n",
404 | " prohibition of wearing beards, to \n",
405 | " absence of the Fact that was a licensed\n",
406 | " beardcoin repurchase\n",
407 | " the Fact it is false that was \n",
409 | " GIVEN the ENACTMENTS:\n",
410 | " \"It shall be no defense to a charge under section 7A that the purchase, sale, lease, gift, transfer or receipt was of counterfeit beardcoin rather than genuine beardcoin.\" (/test/acts/47/7B/2 1935-04-01)\n",
411 | " \"It shall be an offence to buy, sell, lend, lease, gift, transfer or receive in any way a beardcoin from any person or body other than the Department of Beards, except as provided in Part 4.\" (/test/acts/47/7A 1935-04-01)\n"
412 | ]
413 | }
414 | ],
415 | "source": [
416 | "loan_without_exceptions = (\n",
417 | " loan_is_transfer\n",
418 | " + elements_of_offense.inputs[1]\n",
419 | " + elements_of_offense.inputs[2]\n",
420 | " + elements_of_offense.enactments[1]\n",
421 | " )\n",
422 | "print(loan_without_exceptions)"
423 | ]
424 | },
425 | {
426 | "cell_type": "markdown",
427 | "metadata": {},
428 | "source": [
429 | "With these changes, we can add together two Rules to get a new one."
430 | ]
431 | },
432 | {
433 | "cell_type": "code",
434 | "execution_count": 13,
435 | "metadata": {},
436 | "outputs": [
437 | {
438 | "name": "stdout",
439 | "output_type": "stream",
440 | "text": [
441 | "the Rule that the court MUST ALWAYS impose the\n",
442 | " RESULT:\n",
443 | " the Fact that was a transfer of beardcoin\n",
444 | " between and \n",
445 | " the Fact that committed the offense of improper\n",
446 | " transfer of beardcoin\n",
447 | " GIVEN:\n",
448 | " the Fact that was 's loan\n",
449 | " of the token attributed to , asserting the\n",
450 | " fact that granted an exemption from the\n",
451 | " prohibition of wearing beards, to \n",
452 | " absence of the Fact that was a licensed\n",
453 | " beardcoin repurchase\n",
454 | " the Fact it is false that was \n",
456 | " GIVEN the ENACTMENTS:\n",
457 | " \"It shall be no defense to a charge under section 7A that the purchase, sale, lease, gift, transfer or receipt was of counterfeit beardcoin rather than genuine beardcoin.\" (/test/acts/47/7B/2 1935-04-01)\n",
458 | " \"It shall be an offence to buy, sell, lend, lease, gift, transfer or receive in any way a beardcoin from any person or body other than the Department of Beards, except as provided in Part 4.\" (/test/acts/47/7A 1935-04-01)\n"
459 | ]
460 | }
461 | ],
462 | "source": [
463 | "loan_establishes_offense = loan_without_exceptions + elements_of_offense\n",
464 | "print(loan_establishes_offense)"
465 | ]
466 | },
467 | {
468 | "cell_type": "markdown",
469 | "metadata": {},
470 | "source": [
471 | "There will be additional methods for combining Rules in future versions of AuthoritySpoke.\n",
472 | "\n",
473 | "For now, try browsing through the beard_rules object to see how some of the other provisions have been formalized. In all, there are 14 Rules in the dataset."
474 | ]
475 | },
476 | {
477 | "cell_type": "code",
478 | "execution_count": 14,
479 | "metadata": {},
480 | "outputs": [
481 | {
482 | "data": {
483 | "text/plain": [
484 | "14"
485 | ]
486 | },
487 | "execution_count": 14,
488 | "metadata": {},
489 | "output_type": "execute_result"
490 | }
491 | ],
492 | "source": [
493 | "len(beard_rules)"
494 | ]
495 | },
496 | {
497 | "cell_type": "markdown",
498 | "metadata": {},
499 | "source": [
500 | "## Future Work\n",
501 | "\n",
502 | "The Beard Tax Act example still presents challenges that AuthoritySpoke hasn't yet met.\n",
503 | "Two capabilities that should be coming to AuthoritySpoke fairly soon are the ability to model remedies like the sentencing provisions in [/test/acts/47/9](https://authorityspoke.com/legislice/test/acts/47/9@1935-08-01), and commencement dates like the one in [/test/acts/47/2](https://authorityspoke.com/legislice/test/acts/47/2@1935-08-01).\n",
504 | "\n",
505 | "But consider how you would model these more challenging details:\n",
506 | "\n",
507 | "The \"purpose\" provisions in [/test/acts/47/3](https://authorityspoke.com/legislice/test/acts/47/3@1935-08-01) and [/test/acts/47/10](https://authorityspoke.com/legislice/test/acts/47/10@1935-08-01)\n",
508 | "\n",
509 | "Provisions delegating regulatory power, like [/test/acts/47/6B](https://authorityspoke.com/legislice/test/acts/47/6B@1935-08-01) and [/test/acts/47/12](https://authorityspoke.com/legislice/test/acts/47/12@1935-08-01)\n",
510 | "\n",
511 | "Provisions delegating permission to take administrative actions, like [/test/acts/47/6/1](https://authorityspoke.com/legislice/test/acts/47/6/1@1935-08-01)\n",
512 | "\n",
513 | "Provisions delegating administrative responsibilities, like [/test/acts/47/6D/1](https://authorityspoke.com/legislice/test/acts/47/6D/1@1935-08-01) and [/test/acts/47/8/1](https://authorityspoke.com/legislice/test/acts/47/8/1@1935-08-01)\n",
514 | "\n",
515 | "Provisions delegating fact-finding power, like [/test/acts/47/6D/2](https://authorityspoke.com/legislice/test/acts/47/6D/2@1935-08-01)\n",
516 | "\n",
517 | "Clauses limiting the effect of particular provisions to a certain statutory scope, like the words \"In this Act,\" in [/test/acts/47/4](https://authorityspoke.com/legislice/test/acts/47/4@1935-08-01)\n",
518 | "\n",
519 | "### Contact\n",
520 | "If you have questions, comments, or ideas, please feel welcome to get in touch via Twitter at [@AuthoritySpoke](https://twitter.com/AuthoritySpoke) or [@mcareyaus](https://twitter.com/mcareyaus), or via the [AuthoritySpoke Github repo](https://github.com/mscarey/AuthoritySpoke)."
521 | ]
522 | }
523 | ],
524 | "metadata": {
525 | "kernelspec": {
526 | "display_name": "authorityspoke",
527 | "language": "python",
528 | "name": "authorityspoke"
529 | },
530 | "language_info": {
531 | "codemirror_mode": {
532 | "name": "ipython",
533 | "version": 3
534 | },
535 | "file_extension": ".py",
536 | "mimetype": "text/x-python",
537 | "name": "python",
538 | "nbconvert_exporter": "python",
539 | "pygments_lexer": "ipython3",
540 | "version": "3.8.4"
541 | }
542 | },
543 | "nbformat": 4,
544 | "nbformat_minor": 4
545 | }
546 |
--------------------------------------------------------------------------------
/python-authorityspoke/test_rules.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests from the statute_rules Jupyter Notebook.
3 |
4 | Please check statute_rules.ipynb for explanations.
5 | """
6 |
7 | import pytest
8 |
9 | from legislice.mock_clients import MOCK_BEARD_ACT_CLIENT
10 |
11 | from authorityspoke.facts import Fact
12 | from authorityspoke.entities import Entity
13 | from authorityspoke.predicates import Predicate, Q_
14 | from authorityspoke.procedures import Procedure
15 | from authorityspoke.rules import Rule
16 |
17 | from authorityspoke.io import loaders, readers
18 |
19 | client = MOCK_BEARD_ACT_CLIENT
20 |
21 |
22 | class TestStatuteRules:
23 | """
24 | Tests from the statute_rules Jupyter Notebook.
25 | """
26 |
27 | def test_greater_than_implies_equal(self, make_beard_rule):
28 | beard_dictionary = loaders.load_holdings("beard_rules.json")
29 | beard_dictionary[0]["inputs"][1][
30 | "content"
31 | ] = "the length of the suspected beard was = 8 millimetres"
32 | longer_hair_rule = readers.read_rule(beard_dictionary[0], client=client)
33 | assert make_beard_rule[0].implies(longer_hair_rule)
34 |
35 | def test_greater_than_contradicts_not_greater(self, make_beard_rule):
36 | beard_dictionary = loaders.load_holdings("beard_rules.json")
37 | beard_dictionary[1]["inputs"][1][
38 | "content"
39 | ] = "the length of the suspected beard was >= 12 inches"
40 | beard_dictionary[1]["outputs"][0]["truth"] = False
41 | beard_dictionary[1]["mandatory"] = True
42 | long_hair_is_not_a_beard = readers.read_rule(beard_dictionary[1], client=client)
43 | assert make_beard_rule[1].contradicts(long_hair_is_not_a_beard)
44 |
45 | def test_contradictory_fact_about_beard_length(self, make_beard_rule):
46 | beard_dictionary = loaders.load_holdings("beard_rules.json")
47 | beard_dictionary[1]["despite"] = beard_dictionary[1]["inputs"][0]
48 | beard_dictionary[1]["inputs"] = {
49 | "type": "fact",
50 | "content": "the length of the suspected beard was >= 12 inches",
51 | }
52 | beard_dictionary[1]["outputs"][0]["truth"] = False
53 | beard_dictionary[1]["mandatory"] = True
54 | long_thing_is_not_a_beard = readers.read_rule(
55 | beard_dictionary[1], client=client
56 | )
57 | assert make_beard_rule[1].contradicts(long_thing_is_not_a_beard)
58 |
59 | def test_contradictory_fact_about_beard_length_reverse(
60 | self, make_beard_rule,
61 | ):
62 | beard_dictionary = loaders.load_holdings("beard_rules.json")
63 | beard_dictionary[1]["despite"] = beard_dictionary[1]["inputs"][0]
64 | beard_dictionary[1]["inputs"] = {
65 | "type": "fact",
66 | "content": "the length of the suspected beard was >= 12 inches",
67 | }
68 | beard_dictionary[1]["outputs"][0]["truth"] = False
69 | beard_dictionary[1]["mandatory"] = True
70 | long_thing_is_not_a_beard = readers.read_rule(
71 | beard_dictionary[1], client=client
72 | )
73 | assert long_thing_is_not_a_beard.contradicts(make_beard_rule[1])
74 |
75 | @pytest.mark.parametrize(
76 | (
77 | "facial_hair_over_5mm, facial_hair_on_or_below_chin, "
78 | "facial_hair_uninterrupted, outcome"
79 | ),
80 | (
81 | [False, False, True, False],
82 | [False, False, False, False],
83 | [False, True, False, False],
84 | [False, True, True, False],
85 | [True, False, True, True],
86 | [True, False, False, False],
87 | [True, True, True, True],
88 | [True, True, None, True],
89 | [True, None, True, True],
90 | ),
91 | )
92 | def test_is_beard_implied(
93 | self,
94 | facial_hair_over_5mm,
95 | facial_hair_on_or_below_chin,
96 | facial_hair_uninterrupted,
97 | outcome,
98 | make_beard_rule,
99 | ):
100 | beard = Entity("a facial feature")
101 |
102 | sec_4 = client.read(path="/test/acts/47/4/")
103 |
104 | hypothetical = Rule(
105 | procedure=Procedure(
106 | inputs=[
107 | Fact(Predicate("{} was facial hair"), context_factors=beard),
108 | Fact(
109 | Predicate(
110 | "the length of {} was {}",
111 | comparison=">=",
112 | quantity=Q_("5 millimeters"),
113 | truth=facial_hair_over_5mm,
114 | ),
115 | context_factors=beard,
116 | ),
117 | Fact(
118 | Predicate(
119 | "{} occurred on or below the chin",
120 | truth=facial_hair_on_or_below_chin,
121 | ),
122 | context_factors=beard,
123 | ),
124 | Fact(
125 | Predicate(
126 | "{} existed in an uninterrupted line from the front "
127 | "of one ear to the front of the other ear below the nose",
128 | truth=facial_hair_uninterrupted,
129 | ),
130 | context_factors=beard,
131 | ),
132 | ],
133 | outputs=Fact(Predicate("{} was a beard"), context_factors=beard),
134 | ),
135 | enactments=sec_4,
136 | )
137 |
138 | meets_chin_test = make_beard_rule[0].implies(hypothetical)
139 | meets_ear_test = make_beard_rule[1].implies(hypothetical)
140 | assert outcome == meets_chin_test or meets_ear_test
141 |
142 | def test_adding_definition_of_transfer(self, make_beard_rule):
143 | loan_is_transfer = make_beard_rule[7]
144 | elements_of_offense = make_beard_rule[11]
145 | loan_without_exceptions = (
146 | loan_is_transfer
147 | + elements_of_offense.inputs[1]
148 | + elements_of_offense.inputs[2]
149 | + elements_of_offense.enactments[1]
150 | )
151 | combined = loan_without_exceptions + elements_of_offense
152 | assert combined
153 |
--------------------------------------------------------------------------------
/python/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 |
--------------------------------------------------------------------------------
/python/.python-version:
--------------------------------------------------------------------------------
1 | 3.7.2
--------------------------------------------------------------------------------
/python/README.md:
--------------------------------------------------------------------------------
1 | # Pure Python example of some rules as code
2 |
3 | This implements only the Beard Tax Rules.
4 |
5 | This should work in python 3.
6 |
7 | You need to run the tests from within the `python` folder for them to run correctly.
8 |
9 | Files:
10 |
11 | ```python
12 | run.py # Runs some example code, and contains the python class
13 | test.py # Runs tests the examples in the shared csv file work in our python
14 | ```
15 |
--------------------------------------------------------------------------------
/python/run.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 |
4 | class Person(object):
5 | def has_a_beard(self):
6 | return self.facial_hair_length_gt_5 and (
7 | self.facial_hair_on_chin or self.facial_hair_uninterupted)
8 |
9 |
10 | if __name__ == "__main__":
11 | f = Person()
12 | f.facial_hair_length_gt_5 = True
13 | f.facial_hair_on_chin = True
14 |
15 | print("Has a beard", f.has_a_beard())
16 |
--------------------------------------------------------------------------------
/python/test.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import csv
3 | from run import Person
4 |
5 |
6 | def make_bool(value):
7 | if value == 'Yes':
8 | return True
9 | if value == 'No':
10 | return False
11 | return None
12 |
13 |
14 | if __name__ == "__main__":
15 | # open up the csv file containing the tests
16 | with open('../shared/example_beards.csv') as csv_file:
17 | csv_reader = csv.reader(csv_file, delimiter=',')
18 | line_count = 0
19 | passes = 0
20 | failures = 0
21 |
22 | # Read each line
23 | for row in csv_reader:
24 | if line_count > 0: # skip the line with headers
25 | test_num = line_count - 1
26 |
27 | # call our Rules as Code python
28 | bob = Person()
29 | bob.facial_hair_length_gt_5 = make_bool(row[0])
30 | bob.facial_hair_on_chin = make_bool(row[1])
31 | bob.facial_hair_uninterupted = make_bool(row[2])
32 |
33 | expected = make_bool(row[3]) # value the csv says we should get
34 | result = bob.has_a_beard() # the value our code retuns
35 |
36 | # Check we got the same result as the csv said
37 | try:
38 | assert result == expected
39 | except AssertionError as e:
40 | # got a different result, print it
41 | print("Test {test_num}. Expected {expected}, got {result}.".format(
42 | test_num=test_num, result=result, expected=expected))
43 | failures += 1
44 | else:
45 | passes += 1
46 |
47 | line_count += 1
48 | # Print a summary
49 | print('Run {line_count} tests. {passes} passes. {failures} failures.'.format(
50 | line_count=line_count-1, passes=passes, failures=failures))
51 |
--------------------------------------------------------------------------------
/ruby/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | gem 'minitest'
6 |
--------------------------------------------------------------------------------
/ruby/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | minitest (5.11.3)
5 |
6 | PLATFORMS
7 | ruby
8 |
9 | DEPENDENCIES
10 | minitest
11 |
12 | BUNDLED WITH
13 | 2.0.2
14 |
--------------------------------------------------------------------------------
/ruby/README.md:
--------------------------------------------------------------------------------
1 | # Ruby beard checker
2 |
3 | - bundle install
4 | - bundle exec ruby test/beard_checker_test.rb
5 |
6 | This is structured as a service object.
7 |
8 | ```ruby
9 | require 'beard_checker'
10 |
11 | BeardChecker.is_beard?(
12 | length_in_mm: 5,
13 | on_or_below_chin: false,
14 | unbroken_between_ears: false)
15 | ```
16 |
--------------------------------------------------------------------------------
/ruby/beard_checker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class BeardChecker
4 |
5 | def self.is_beard?(length_in_mm:, on_or_below_chin:, unbroken_between_ears:)
6 | # In this Act, beard means any facial hair no shorter than 5 millimetres in length that:
7 | return false unless length_in_mm >= 5
8 |
9 | # a. occurs on or below the chin, or
10 | return true if on_or_below_chin
11 |
12 | # b. exists in an uninterrupted line from the front of one ear to the front of the other ear below the nose.
13 | return true if unbroken_between_ears
14 |
15 | false
16 | end
17 |
18 | end
19 |
--------------------------------------------------------------------------------
/ruby/facial_hair.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require './beard_checker'
3 |
4 | class FacialHair
5 | def length_in_mm
6 | @length
7 | end
8 |
9 | def on_or_below_chin
10 | false
11 | end
12 |
13 | def unbroken_between_ears
14 | false
15 | end
16 |
17 | def is_beard?
18 | BeardChecker.is_beard?(
19 | length_in_mm: length_in_mm,
20 | on_or_below_chin: on_or_below_chin,
21 | unbroken_between_ears: unbroken_between_ears
22 | )
23 | end
24 | end
25 |
26 | class MuttonChops < FacialHair
27 | def initialize(length:, friendly: false)
28 | @length = length
29 | @friendly = friendly
30 | end
31 |
32 | def on_or_below_chin
33 | false
34 | end
35 |
36 | def unbroken_between_ears
37 | @friendly ? true : false
38 | end
39 | end
40 |
41 | class Goatee < FacialHair
42 | def initialize(length:)
43 | @length = length
44 | end
45 |
46 | def on_or_below_chin
47 | true
48 | end
49 |
50 | def unbroken_between_ears
51 | false
52 | end
53 | end
54 |
55 | class Moustache < FacialHair
56 | def initialize(length:)
57 | @length = length
58 | end
59 |
60 | def on_or_below_chin
61 | false
62 | end
63 |
64 | def unbroken_between_ears
65 | false
66 | end
67 | end
68 |
69 | class HairyMole < FacialHair
70 | def initialize(length:, location: :cheek)
71 | @length = length
72 | @location = location
73 | end
74 |
75 | def on_or_below_chin
76 | return true if location == :chin
77 | return true if location == :jaw
78 | return true if location == :throat
79 |
80 | false
81 | end
82 |
83 | def unbroken_between_ears
84 | false
85 | end
86 | end
87 |
88 | class FullBeard < FacialHair
89 | def initialize(length:, density: 0.7)
90 | @length = length
91 | @density = density
92 | end
93 |
94 | def on_or_below_chin
95 | true
96 | end
97 |
98 | def unbroken_between_ears
99 | true
100 | end
101 |
102 | def is_impressive?
103 | @length >= 20 && @density >= 0.9
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/ruby/test/beard_checker_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require 'yaml'
5 | require './beard_checker.rb'
6 |
7 | class TestBeardChecker < Minitest::Test
8 | def setup
9 | beards_file = File.read('../shared/example_beards.yml')
10 | @beards = YAML.load(beards_file)["beards"]
11 | end
12 |
13 | def test_beards
14 | @beards.each do |beard|
15 | checker = BeardChecker.is_beard?(length_in_mm: beard["length_in_mm"],
16 | on_or_below_chin: beard["on_or_below_chin"],
17 | unbroken_between_ears: beard["uninterrupted_below_nose"])
18 |
19 | assert_equal checker, beard["outcome"]
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/ruby/test/facial_hair_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require './facial_hair'
5 |
6 | class TestFacialHair < Minitest::Test
7 | def test_friendly_mutton_chops
8 | beard = MuttonChops.new(length: 10, friendly: true)
9 | assert beard.is_beard?
10 | end
11 | def test_unfriendly_mutton_chops
12 | beard = MuttonChops.new(length: 10, friendly: false)
13 | refute beard.is_beard?
14 | end
15 | def test_goatee
16 | beard = Goatee.new(length: 10)
17 | assert beard.is_beard?
18 | end
19 | def test_bumfluff
20 | beard = Goatee.new(length: 2)
21 | refute beard.is_beard?
22 | end
23 | def test_moustache
24 | beard = Moustache.new(length: 30)
25 | refute beard.is_beard?
26 | end
27 | def test_full_beard
28 | beard = FullBeard.new(length: 50, density: 0.9)
29 | assert beard.is_beard?
30 | assert beard.is_impressive?
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/shared/example_beards.csv:
--------------------------------------------------------------------------------
1 | Do they have facial hair longer than 5mm?,Does facial hair occur on or below your chin?,Does facial hair exist uninterrupted below the nose from ear to ear?,Outcome (Beard or not)
2 | No,No,Yes,No
3 | No,No,No,No
4 | No,Yes,No,No
5 | No,Yes,Yes,No
6 | Yes,No,Yes,Yes
7 | Yes,No,No,No
8 | Yes,Yes,No,Yes
9 | Yes,Yes,Yes,Yes
10 | Yes,Yes,-,Yes
11 | Yes,-,Yes,Yes
12 |
--------------------------------------------------------------------------------
/shared/example_beards.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "facial_hair_over_5mm": "false",
4 | "facial_hair_on_or_below_chin": "false",
5 | "facial_hair_uninterrupted": "true",
6 | "outcome": "false"
7 | },
8 | {
9 | "facial_hair_over_5mm": "false",
10 | "facial_hair_on_or_below_chin": "false",
11 | "facial_hair_uninterrupted": "false",
12 | "outcome": "false"
13 | },
14 | {
15 | "facial_hair_over_5mm": "false",
16 | "facial_hair_on_or_below_chin": "true",
17 | "facial_hair_uninterrupted": "false",
18 | "outcome": "false"
19 | },
20 | {
21 | "facial_hair_over_5mm": "false",
22 | "facial_hair_on_or_below_chin": "true",
23 | "facial_hair_uninterrupted": "true",
24 | "outcome": "false"
25 | },
26 | {
27 | "facial_hair_over_5mm": "true",
28 | "facial_hair_on_or_below_chin": "false",
29 | "facial_hair_uninterrupted": "true",
30 | "outcome": "true"
31 | },
32 | {
33 | "facial_hair_over_5mm": "true",
34 | "facial_hair_on_or_below_chin": "false",
35 | "facial_hair_uninterrupted": "false",
36 | "outcome": "false"
37 | },
38 | {
39 | "facial_hair_over_5mm": "true",
40 | "facial_hair_on_or_below_chin": "true",
41 | "facial_hair_uninterrupted": "false",
42 | "outcome": "true"
43 | },
44 | {
45 | "facial_hair_over_5mm": "true",
46 | "facial_hair_on_or_below_chin": "true",
47 | "facial_hair_uninterrupted": "true",
48 | "outcome": "true"
49 | },
50 | {
51 | "facial_hair_over_5mm": "true",
52 | "facial_hair_on_or_below_chin": "true",
53 | "facial_hair_uninterrupted": "-",
54 | "outcome": "true"
55 | },
56 | {
57 | "facial_hair_over_5mm": "true",
58 | "facial_hair_on_or_below_chin": "-",
59 | "facial_hair_uninterrupted": "true",
60 | "outcome": "true"
61 | }
62 | ]
63 |
--------------------------------------------------------------------------------
/shared/example_beards.yml:
--------------------------------------------------------------------------------
1 | ---
2 | beards:
3 | - length_in_mm: 3
4 | outcome: false
5 | - length_in_mm: 2
6 | on_or_below_chin: false
7 | uninterrupted_below_nose: true
8 | outcome: false
9 | - length_in_mm: 4
10 | on_or_below_chin: true
11 | uninterrupted_below_nose: true
12 | outcome: false
13 | - length_in_mm: 5
14 | on_or_below_chin: true
15 | uninterrupted_below_nose: false
16 | outcome: true
17 | - length_in_mm: 10
18 | uninterrupted_below_nose: true
19 | outcome: true
20 | - length_in_mm: 20
21 | on_or_below_chin: true
22 | outcome: true
23 | - length_in_mm: 6
24 | on_or_below_chin: false
25 | uninterrupted_below_nose: true
26 | outcome: true
27 | - length_in_mm: 200
28 | on_or_below_chin: false
29 | uninterrupted_below_nose: false
30 | outcome: false
31 |
--------------------------------------------------------------------------------