Xalgorithms / general-examples

A repository used for rule package examples. May eventually contain core rules.
Apache License 2.0
1 stars 2 forks source link

A new rule example: 'delivery policy of a grocery store' #19

Open jpotvin opened 4 years ago

jpotvin commented 4 years ago

Could you please write this example in the latest Xalgo? In the formal paper I'd like to finish off this comparison with our method. natural-structured-tree-table_comparison.pdf

RyanFleck commented 4 years ago

This rule returns seperate tables for grocery orders that are no_delivery, charged_delivery, and free_delivery.

grocery_delivery_policy.rule

META
     VERSION "0.0.1"
     RUNTIME "0.4.0"
     CRITICALITY "experimental"

     MANAGER "Joseph Potvin <jpotvin@xalgorithms.org>"
     AUTHOR "Joseph Potvin <jpotvin@xalgorithms.org>"
     MAINTAINER "Ryan Fleck <ryan.fleck@protonmail.com>";

EFFECTIVE
     IN "CA-ON"
     FROM "2018-04-01T00:00"
     TO "9999-12-30T23:59"
     TIMEZONE "America/Toronto"

WHEN envelope:type == "delivery_inquiry";
WHEN envelope:organization == "abc_grocers";

WHEN items:delivery_is_available == "X";
WHEN items:delivered == "N";

REQUIRE org.xalgorithms.examples.grocery_delivery_records:0.0.1 AS table_origin;

ASSEMBLE delivery_data
 COLUMNS ( "shipment_id", "delivery_is_available", "box_type_is_standard", "box_half_full", "box_contents_value", "delivered" ) FROM table_origin; // Everything but the contents of the order.

REFINE delivery_data AS no_delivery
    FILTER "box_half_full" == "Y";

REFINE delivery_data AS charged_delivery_standard
    FILTER "box_half_full" != "Y"
    FILTER "box_type_is_standard" != "Y"
    FILTER "box_contents_value" > 100.00

REFINE delivery_data AS charged_delivery_non_standard
    FILTER "box_half_full" != "Y"
    FILTER "box_type_is_standard" != "N"

ASSEMBLE charged_delivery
    COLUMNS FROM charged_delivery_standard
    COLUMNS FROM charged_delivery_non_standard

REFINE delivery_data AS free_delivery
    FILTER "box_half_full" != "Y"
    FILTER "box_type_is_standard" == "N"
    FILTER "box_contents_value" < 100.00

grocery_delivery_records.table

META
 VERSION "0.0.1"
 RUNTIME "0.4.0"
 CRITICALITY "experimental"
 MANAGER "Joseph Potvin <jpotvin@xalgorithms.org>"
 AUTHOR "Joseph Potvin <jpotvin@xalgorithms.org>"
 MAINTAINER "Ryan Fleck <ryan.fleck@protonmail.com>";

DATA grocery_delivery_records.json

grocery_delivery_records.json

[
  {
    "shipment_id":"ZQ430928",
    "delivery_is_available":"",
    "box_half_full":"N",
    "box_type_is_standard":"-",
    "box_contents_value":0.00,
    "delivered":"N",
    "contents":{

    }
  },
  {
    "shipment_id":"BA904209",
    "delivery_is_available":"X",
    "box_volume_occupied > box_capacity/2":"Y",
    "box_type_is_standard":"Y",
    "box_contents_value":78.00,
    "delivered":"N",
    "contents":{

    }
  },
  {
    "shipment_id":"HT5928",
    "delivery_is_available":"X",
    "box_half_full":"Y",
    "box_type_is_standard":"Y",
    "box_contents_value":847.00,
    "delivered":"N",
    "contents":{

    }
  },
  {
    "shipment_id":"HT5928",
    "delivery_is_available":"X",
    "box_half_full":"Y",
    "box_type_is_standard":"N",
    "box_contents_value":14.00,
    "delivered":"N",
    "contents":{

    }
  }
]
karfai commented 4 years ago

This attempt yields a final table with a list of deliveries that should have free shipping. It's incomplete though. I'd like to be able to generate a final table that has the final delivery charge for every package.

First, the tables:

[
  { "id": 1, "box" : "standard",     "volume" : 2, "weight" : 5,  "value" : 100 },
  { "id": 2, "box" : "standard",     "volume" : 5, "weight" : 6,  "value" : 105 },
  { "id": 3, "box" : "non_standard", "volume" : 7, "weight" : 10, "value" : 300 },
  { "id": 4, "box" : "non_standard", "volume" : 3, "weight" : 6,  "value" : 200 },
  { "id": 5, "box" : "standard",     "volume" : 5, "weight" : 6,  "value" : 75  },
  { "id": 6, "box" : "standard",     "volume" : 6, "weight" : 6,  "value" : 120 }
]

The boxes table lists all of the deliveries to be made. It has some columns:

[
  { "name" : "standard",      "capacity" : 6 },
  { "name" : "non_standard",  "capacity" : 8 }
]

The types table contains name and capacity which tells us the full capacity of each type of box.

[
  { "code" : "standard",      "charge" : 4, "min_value" : 0     },
  { "code" : "standard_free", "charge" : 0, "min_value" : 99.99 },
  { "code" : "non_standard",  "charge" : 5, "min_value" : 0     }
]

The costs table lists different types of deliveries, their charge for delivery and the min_value needed for the charge.

The rule:

META
  VERSION "0.0.1"
  MAINTAINER "Don Kelly <karfai@gmail.com>";

REQUIRE grocery:boxes:0.0.1 AS boxes;
REQUIRE grocery:types:0.0.1 AS types;
REQUIRE grocery:costs:0.0.1 AS costs;

ASSEMBLE possibilities
  COLUMNS FROM boxes
  COLUMNS FROM types
  COLUMNS FROM costs;

# standard_free w/ '_' because '-' fails to be a string in the parser
REFINE possibilities AS standard_boxes_with_free
  FILTER @box==@name
  FILTER @code=="standard_free"
  FILTER @name=="standard";

REFINE standard_boxes_with_free AS standard_boxes_over_value
  FILTER @value>@min_value
  MAP over_cap=subtract(@volume, divide(@capacity, 2));

# wart b/c we can't seem to access @over_cap ^^ - probably a bug
REFINE standard_boxes_over_value AS free_deliveries
  MAP charge_id=@id
  FILTER @over_cap>0;

The rule loads all the tables and combines them into a full large dot product (possibilities). This is refined into standard_boxes_with_free that contains matching of the types of boxes in boxes to those in types. It is also filtered to only rows containing boxes of type standard where they have a code of standard_free. We do this because this is the only data we want to work with.

The next REFINE filters to only contain rows where the value is greater than the min_value for the code. It also MAPs a column called over_cap that determines whether the box's volume. This will be used in the next REFINE to select only boxes that are over the volume for free shipping. We do this in two REFINE steps because there seems to be a bug where functions (divide(@capacity, 2)) do not work in FILTER statements. Otherwise, we'd just filter by volume>divide(@capacity, 2).

The final table (free_deliveries) contains the boxes with free shipping:

     0 | weight:              6 | name:       standard | over_cap:              2 | code:  standard_free | id:              2 | min_value:          99.99 | charge:              0 | box:       standard | value:            105 | volume:              5 | capacity:              6 | charge_id:              2 |
     1 | weight:              6 | name:       standard | over_cap:              3 | code:  standard_free | id:              6 | min_value:          99.99 | charge:              0 | box:       standard | value:            120 | volume:              6 | capacity:              6 | charge_id:              6 |
karfai commented 4 years ago

Full output of ☝️:

tables:standard_boxes_over_value
     0 | weight:              5 | name:       standard | over_cap:             -1 | code:  standard_free | id:              1 | min_value:          99.99 | charge:              0 | box:       standard | value:            100 | volume:              2 | capacity:              6 |
     1 | weight:              6 | name:       standard | over_cap:              2 | code:  standard_free | id:              2 | min_value:          99.99 | charge:              0 | box:       standard | value:            105 | volume:              5 | capacity:              6 |
     2 | weight:              6 | name:       standard | over_cap:              3 | code:  standard_free | id:              6 | min_value:          99.99 | charge:              0 | box:       standard | value:            120 | volume:              6 | capacity:              6 |
tables:standard_boxes_with_free
     0 | weight:              5 | name:       standard | code:  standard_free | id:              1 | min_value:          99.99 | charge:              0 | box:       standard | value:            100 | volume:              2 | capacity:              6 |
     1 | weight:              6 | name:       standard | code:  standard_free | id:              2 | min_value:          99.99 | charge:              0 | box:       standard | value:            105 | volume:              5 | capacity:              6 |
     2 | weight:              6 | name:       standard | code:  standard_free | id:              5 | min_value:          99.99 | charge:              0 | box:       standard | value:             75 | volume:              5 | capacity:              6 |
     3 | weight:              6 | name:       standard | code:  standard_free | id:              6 | min_value:          99.99 | charge:              0 | box:       standard | value:            120 | volume:              6 | capacity:              6 |
tables:boxes
     0 | weight:              5 | id:              1 | box:       standard | value:            100 | volume:              2 |
     1 | weight:              6 | id:              2 | box:       standard | value:            105 | volume:              5 |
     2 | weight:             10 | id:              3 | box:   non_standard | value:            300 | volume:              7 |
     3 | weight:              6 | id:              4 | box:   non_standard | value:            200 | volume:              3 |
     4 | weight:              6 | id:              5 | box:       standard | value:             75 | volume:              5 |
     5 | weight:              6 | id:              6 | box:       standard | value:            120 | volume:              6 |
tables:free_deliveries
     0 | weight:              6 | name:       standard | over_cap:              2 | code:  standard_free | id:              2 | min_value:          99.99 | charge:              0 | box:       standard | value:            105 | volume:              5 | capacity:              6 | charge_id:              2 |
     1 | weight:              6 | name:       standard | over_cap:              3 | code:  standard_free | id:              6 | min_value:          99.99 | charge:              0 | box:       standard | value:            120 | volume:              6 | capacity:              6 | charge_id:              6 |
tables:possibilities
     0 | weight:              5 | name:       standard | code:       standard | id:              1 | min_value:              0 | charge:              4 | box:       standard | value:            100 | volume:              2 | capacity:              6 |
     1 | weight:              5 | name:       standard | code:  standard_free | id:              1 | min_value:          99.99 | charge:              0 | box:       standard | value:            100 | volume:              2 | capacity:              6 |
     2 | weight:              5 | name:       standard | code:   non_standard | id:              1 | min_value:              0 | charge:              5 | box:       standard | value:            100 | volume:              2 | capacity:              6 |
     3 | weight:              5 | name:   non_standard | code:       standard | id:              1 | min_value:              0 | charge:              4 | box:       standard | value:            100 | volume:              2 | capacity:              8 |
     4 | weight:              5 | name:   non_standard | code:  standard_free | id:              1 | min_value:          99.99 | charge:              0 | box:       standard | value:            100 | volume:              2 | capacity:              8 |
     5 | weight:              5 | name:   non_standard | code:   non_standard | id:              1 | min_value:              0 | charge:              5 | box:       standard | value:            100 | volume:              2 | capacity:              8 |
     6 | weight:              6 | name:       standard | code:       standard | id:              2 | min_value:              0 | charge:              4 | box:       standard | value:            105 | volume:              5 | capacity:              6 |
     7 | weight:              6 | name:       standard | code:  standard_free | id:              2 | min_value:          99.99 | charge:              0 | box:       standard | value:            105 | volume:              5 | capacity:              6 |
     8 | weight:              6 | name:       standard | code:   non_standard | id:              2 | min_value:              0 | charge:              5 | box:       standard | value:            105 | volume:              5 | capacity:              6 |
     9 | weight:              6 | name:   non_standard | code:       standard | id:              2 | min_value:              0 | charge:              4 | box:       standard | value:            105 | volume:              5 | capacity:              8 |
    10 | weight:              6 | name:   non_standard | code:  standard_free | id:              2 | min_value:          99.99 | charge:              0 | box:       standard | value:            105 | volume:              5 | capacity:              8 |
    11 | weight:              6 | name:   non_standard | code:   non_standard | id:              2 | min_value:              0 | charge:              5 | box:       standard | value:            105 | volume:              5 | capacity:              8 |
    12 | weight:             10 | name:       standard | code:       standard | id:              3 | min_value:              0 | charge:              4 | box:   non_standard | value:            300 | volume:              7 | capacity:              6 |
    13 | weight:             10 | name:       standard | code:  standard_free | id:              3 | min_value:          99.99 | charge:              0 | box:   non_standard | value:            300 | volume:              7 | capacity:              6 |
    14 | weight:             10 | name:       standard | code:   non_standard | id:              3 | min_value:              0 | charge:              5 | box:   non_standard | value:            300 | volume:              7 | capacity:              6 |
    15 | weight:             10 | name:   non_standard | code:       standard | id:              3 | min_value:              0 | charge:              4 | box:   non_standard | value:            300 | volume:              7 | capacity:              8 |
    16 | weight:             10 | name:   non_standard | code:  standard_free | id:              3 | min_value:          99.99 | charge:              0 | box:   non_standard | value:            300 | volume:              7 | capacity:              8 |
    17 | weight:             10 | name:   non_standard | code:   non_standard | id:              3 | min_value:              0 | charge:              5 | box:   non_standard | value:            300 | volume:              7 | capacity:              8 |
    18 | weight:              6 | name:       standard | code:       standard | id:              4 | min_value:              0 | charge:              4 | box:   non_standard | value:            200 | volume:              3 | capacity:              6 |
    19 | weight:              6 | name:       standard | code:  standard_free | id:              4 | min_value:          99.99 | charge:              0 | box:   non_standard | value:            200 | volume:              3 | capacity:              6 |
    20 | weight:              6 | name:       standard | code:   non_standard | id:              4 | min_value:              0 | charge:              5 | box:   non_standard | value:            200 | volume:              3 | capacity:              6 |
    21 | weight:              6 | name:   non_standard | code:       standard | id:              4 | min_value:              0 | charge:              4 | box:   non_standard | value:            200 | volume:              3 | capacity:              8 |
    22 | weight:              6 | name:   non_standard | code:  standard_free | id:              4 | min_value:          99.99 | charge:              0 | box:   non_standard | value:            200 | volume:              3 | capacity:              8 |
    23 | weight:              6 | name:   non_standard | code:   non_standard | id:              4 | min_value:              0 | charge:              5 | box:   non_standard | value:            200 | volume:              3 | capacity:              8 |
    24 | weight:              6 | name:       standard | code:       standard | id:              5 | min_value:              0 | charge:              4 | box:       standard | value:             75 | volume:              5 | capacity:              6 |
    25 | weight:              6 | name:       standard | code:  standard_free | id:              5 | min_value:          99.99 | charge:              0 | box:       standard | value:             75 | volume:              5 | capacity:              6 |
    26 | weight:              6 | name:       standard | code:   non_standard | id:              5 | min_value:              0 | charge:              5 | box:       standard | value:             75 | volume:              5 | capacity:              6 |
    27 | weight:              6 | name:   non_standard | code:       standard | id:              5 | min_value:              0 | charge:              4 | box:       standard | value:             75 | volume:              5 | capacity:              8 |
    28 | weight:              6 | name:   non_standard | code:  standard_free | id:              5 | min_value:          99.99 | charge:              0 | box:       standard | value:             75 | volume:              5 | capacity:              8 |
    29 | weight:              6 | name:   non_standard | code:   non_standard | id:              5 | min_value:              0 | charge:              5 | box:       standard | value:             75 | volume:              5 | capacity:              8 |
    30 | weight:              6 | name:       standard | code:       standard | id:              6 | min_value:              0 | charge:              4 | box:       standard | value:            120 | volume:              6 | capacity:              6 |
    31 | weight:              6 | name:       standard | code:  standard_free | id:              6 | min_value:          99.99 | charge:              0 | box:       standard | value:            120 | volume:              6 | capacity:              6 |
    32 | weight:              6 | name:       standard | code:   non_standard | id:              6 | min_value:              0 | charge:              5 | box:       standard | value:            120 | volume:              6 | capacity:              6 |
    33 | weight:              6 | name:   non_standard | code:       standard | id:              6 | min_value:              0 | charge:              4 | box:       standard | value:            120 | volume:              6 | capacity:              8 |
    34 | weight:              6 | name:   non_standard | code:  standard_free | id:              6 | min_value:          99.99 | charge:              0 | box:       standard | value:            120 | volume:              6 | capacity:              8 |
    35 | weight:              6 | name:   non_standard | code:   non_standard | id:              6 | min_value:              0 | charge:              5 | box:       standard | value:            120 | volume:              6 | capacity:              8 |
tables:types
     0 | name:       standard | capacity:              6 |
     1 | name:   non_standard | capacity:              8 |
tables:costs
     0 | code:       standard | charge:              4 | min_value:              0 |
     1 | code:  standard_free | charge:              0 | min_value:          99.99 |
     2 | code:   non_standard | charge:              5 | min_value:              0 |
jpotvin commented 4 years ago

I still need to step through fully what you have above, but here is a possible target Xalgo expression for this which contains all the required information (I reckon) and is readily understandable and expressible by a non-technical 'rule author':

META; |R4qG9UeKH5hW| : “ruleid”; |0.0.1| : “version”; |0.5.0| : “runtime”; |experimental| : “criticality”; |010 001 100 001 100 010 010 111 011| : “semantics”; |Joseph Potvin|<jpotvin@xalgorithms.org| : “manager”; |Joseph Potvin|<jpotvin@xalgorithms.org| : “author”; |Ryan Fleck|ryan.fleck@protonmail.com| : “maintainer”; GIVEN; |CA|ON| : “jurisdictions”; |2018-04-01T00:00|9999-12-30T23:50| : “dates-times”; |UTC-05:00| : “timezones”; WHEN; 0|A|B|C|D| 1|N|Y|Y|N| : "box_at_least_half_full"; 2|-|Y|Y|N| : "box_type_is_standard"; 3|-|N|Y|-| : "box_contents_value" >= $100.00; THEN; 4| |X|X|X| : "delivery_is_available"; 5| |X| |X| : "delivery_is_charged".

jpotvin commented 4 years ago

Here's what the Singapore Stamp Duty looks like to me, adapting from the grocery store policy example: [NOTE: bah, Git changes underscore to a diagonal line and makes text italic, and I can't find a substitute character that works.]

WHEN 0|A|B|C|D|E|F| : 1|Y|Y|Y|Y|Y|Y| : "envelope:type" == "purchase-order"; 2|Y|Y|Y|Y|Y|Y| : "envelope:parties.buyer.industry.list_id" == "ISIC"; 3|Y|Y|Y|Y|Y|Y| : "envelope:parties.buyer.industry.value == "L6810"; 4|Y|Y|Y|Y|Y|Y| : "item:classification.listname" == "UNSPSC"; 5|Y|Y|Y|Y|Y|Y| : "item:classification.value == "80131600"; 6|Y|Y|Y|Y|Y|Y| : "item:price.currency.code == "SGD"; 7|Y|Y|Y|Y|Y|Y| : "item:price.value" > 0; 8|Y|||||| : "buyerprofile" == "sgc-1"; 9||Y||||| : "buyerprofile" == "sgc-2"; 10|||Y|||| : "buyerprofile" == "sgc-n"; 11||||Y||| : "buyerprofile" == "spr-1"; 12|||||Y|| : "buyerprofile" == "spr-n"; 13||||||Y| : "buyerprofile" == "sfe-n". THEN 14|X|||||| : "absdrate" = 0.00 * price.value; 15||X||||| : "absdrate" = 0.07 * price.value; 16|||X|||| : "absdrate" = 0.10 * price.value; 17||||X||| : "absdrate" = 0.05 * price.value; 18|||||X|| : "absdrate" = 0.10 * price.value; 19||||||X| : "absd_rate" = 0.15 * price.value;