Home > Software design >  How to build a decision table with "NOT IN (a,b,c)" logic?
How to build a decision table with "NOT IN (a,b,c)" logic?

Time:12-09

Suppose there is a business rule used to derive a value and due to how the business users need to update this, the logic must be held in a table and not a function.

Example of current function

 WHEN inputA IN (1,2,3) AND inputB NOT IN (55,66) THEN OUTPUT = 'HQ'
 WHEN inputA IN (3,6)   AND inputB     IN (27,44) THEN OUTPUT = 'Northern'
 WHEN inputC IN (6,4,1) AND inputB NOT IN (55,66) THEN OUTPUT = 'Eastern'
 etc.

I can build a table with a row for each combination e.g.

OUTPUT A val B val C val D val ...
Northern 3 27
Northern 3 44
Northern 6 27
Northern 6 44

How then to model the "NOT IN" part of this?

Surely there is a way where I do not have to create all the 'exception' rows that would potentially need to be updated based on changing reference data?

CodePudding user response:

You could represent these expressions w/ a table where each row contained:

  • Output
  • Equals? (true or false)
  • Input
  • Value

which would represent "If (INPUT = VALUE) = EQUALS? then OUTPUT", except that you only need 1 satisfying row for a given OUTPUT/INPUT where EQUALS? is true, but all rows must be satisfied where EQUALS? is false.

This assumes your have only one "rule" for each distinct output.

CodePudding user response:

Here's an attempt at using a rule table with positive and negative rules.

The rule table stores the valids as integers, but the invalids in a string with delimiters. (not sure if json or xml would be better)

But having negative rules makes it really tricky.
Because it's easy to ignore a rule by adding another for the same output name.

Personally I think it's safer to use a UDF for this type of rules.

create table test (
 id int identity(1,1) primary key, 
 inputA int, 
 inputB int, 
 inputC int
)

create table output_areas (
 code varchar(2) primary key, 
 name varchar(30) not null
)

insert into output_areas values
  ('HQ', 'HQ')
, ('N', 'Northern'), ('E', 'Eastern')
, ('W', 'Westhern'), ('S', 'Southern')
, ('X', 'Extern'), ('Z', 'The Zone')


create table input_rules (
 id int identity(1,1) primary key, 
 output_area_code varchar(2) not null,
 relevance int not null default 0,
 inputA_valid int,
 inputB_valid int,
 inputC_valid int, 
 inputA_invalid varchar(100),
 inputB_invalid varchar(100),
 inputC_invalid varchar(100),
 foreign key (output_area_code) references output_areas(code)
)


insert into input_rules (output_area_code, relevance) 
values ('X', 0);

insert into input_rules (output_area_code, relevance, inputA_valid) 
values ('Z',10, 1);

insert into input_rules (output_area_code, relevance, inputA_valid, inputB_invalid) 
values
  ('HQ', 20, 1, '|55|56|')
, ('HQ', 20, 2, '|55|56|')
, ('HQ', 20, 3, '|55|56|')
;
insert into input_rules (output_area_code, relevance, inputA_valid, inputB_valid) 
values
  ('N', 30, 3, 27), ('N', 30, 3, 44)
, ('N', 30, 6, 27), ('N', 30, 6, 44)
;
insert into input_rules (output_area_code, relevance, inputC_valid, inputB_invalid) 
values
  ('E', 20, 6, '|55|66|' )
, ('E', 20, 4, '|55|66|' )
;


insert into test 
  (inputA, inputB, inputC) values 
  (1, 56, null)
, (3, 44, null), (3, 66, null)
, (1, 66, null), (1, 88, null)
, (null, 66, 6), (null, 88, 6)
select * from input_rules
id | output_area_code | relevance | inputA_valid | inputB_valid | inputC_valid | inputA_invalid | inputB_invalid | inputC_invalid
-: | :--------------- | --------: | -----------: | -----------: | -----------: | :------------- | :------------- | :-------------
 1 | X                |         0 |         null |         null |         null | null           | null           | null          
 2 | Z                |        10 |            1 |         null |         null | null           | null           | null          
 3 | HQ               |        20 |            1 |         null |         null | null           | |55|56|        | null          
 4 | HQ               |        20 |            2 |         null |         null | null           | |55|56|        | null          
 5 | HQ               |        20 |            3 |         null |         null | null           | |55|56|        | null          
 6 | N                |        30 |            3 |           27 |         null | null           | null           | null          
 7 | N                |        30 |            3 |           44 |         null | null           | null           | null          
 8 | N                |        30 |            6 |           27 |         null | null           | null           | null          
 9 | N                |        30 |            6 |           44 |         null | null           | null           | null          
10 | E                |        20 |         null |         null |            6 | null           | |55|66|        | null          
11 | E                |        20 |         null |         null |            4 | null           | |55|66|        | null          
select *
from test t
outer apply (
  select top 1 ref.name as output
  from input_rules r
  join output_areas ref on ref.code = r.output_area_code
  where (r.inputA_valid is null or r.inputA_valid = t.inputA) 
    and (r.inputB_valid is null or r.inputB_valid = t.inputB) 
    and (r.inputC_valid is null or r.inputC_valid = t.inputC)
    and (r.inputA_invalid is null or r.inputA_invalid not like concat('%|', t.inputA, '|%')) 
    and (r.inputB_invalid is null or r.inputB_invalid not like concat('%|', t.inputB, '|%')) 
    and (r.inputC_invalid is null or r.inputC_invalid not like concat('%|', t.inputC, '|%')) 
  order by r.relevance desc, r.output_area_code asc
) ca
id | inputA | inputB | inputC | output  
-: | -----: | -----: | -----: | :-------
 1 |      1 |     56 |   null | The Zone
 2 |      3 |     44 |   null | Northern
 3 |      3 |     66 |   null | HQ      
 4 |      1 |     66 |   null | HQ      
 5 |      1 |     88 |   null | HQ      
 6 |   null |     66 |      6 | Extern  
 7 |   null |     88 |      6 | Eastern 

db<>fiddle here

  • Related