Freitag, 5. Februar 2010

Angebotskalkulation - Regelsystem

Nun habe ich mich der Berechnung des Angebotspreises gewidmet. Die Preise sind nämlich von verschiedenen Faktoren (wie zum Beispiel Ausgangsland, Zielland, Wegpunkte etc.) anhängig. Die Preise fix in das Programm zu kodieren wäre sehr unelegant. Aber wie kann man so ein Berechnungssystem möglichst flexibel und trotzdem nicht fix kodiert programmieren? Ich sah von Anfang an zwei vielversprechende Möglichkeiten. Entweder mit einer Scriptsprache (z.B. Script.NET, hab ich schon in meiner Diplomarbeit verwendet) oder über eine XML Datei, in der die Regeln kodiert sind.
Ich hab mich jetzt für die Variante mit der XML Datei entschieden. Dabei soll es verschiedene Rulesets in der Datei geben. Zu jedem RuleSet gehören eine Menge von Regeln. Pro RuleSet kann man sich festlegen, ob man jede Regel ausführen will (evaluate="ALL") oder ob der Interpreter (wie beispielsweise bei iptables) die Regeln nur so lange abarbeiten soll, bis es zu einem Treffer kommt (evaluate="UNTIL_FIRST_HIT"). Wobei erster Treffer nicht ganz zutreffend ist. Jeder Regel sind in meinem Schema nämlich entweder eine variable Anzahl von Kindregeln oder eine Menge von mit der Regel verknüpften Aktionen zugeordnet. Sobald eine Regel erfüllt wurde und ihre Aktionen fertig ausgeführt wurden, wird das in meiner Terminilogie als FIRST_FIT betrachtet.
Das XML Schema für das Regelsystem sieht wie folgt aus:

<?xml version="1.0" encoding="ISO-8859-1"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="unqualified">
<xs:element name="pricing_definition" type="rulesets_Type"/>
<xs:complexType name="rulesets_Type">
<xs:sequence>
<xs:choice maxOccurs="unbounded">
<xs:element name="pricetable" type="pricetable_Type"/>
</xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="geoshape" type="geoshape_Type"/>
</xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="actionset" type="actionset_Type"/>
</xs:choice>
<xs:choice maxOccurs="unbounded">
<xs:element name="ruleset" type="ruleset_Type"/>
</xs:choice>
</xs:sequence>
</xs:complexType>
<xs:complexType name="ruleset_Type">
<xs:sequence maxOccurs="unbounded">
<xs:element name="rule" type="rule_Type"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="evaluate" type="evaluate_type" use="required"/>
</xs:complexType>
<xs:complexType name="rule_Type">
<xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="rule" type="rule_Type"/>
</xs:choice>
<xs:sequence>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="action" type="action_Type"/>
<xs:element name="execute" type="execute_Type"/>
</xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="partial_cargo_pricing" type="partial_cargo_pricing_Type"/>
</xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="panic" type="panic_Type"/>
</xs:choice>
</xs:sequence>
</xs:choice>
<xs:attribute name="match_target" type="match_target_type" use="required"/>
<xs:attribute name="match_type" type="match_type" use="required"/>
<xs:attribute name="match_value" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="execute_Type">
<xs:attribute name="actionset" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="actionset_Type">
<xs:choice>
<xs:sequence>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="action" type="action_Type"/>
<xs:element name="execute" type="execute_Type"/>
</xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="partial_cargo_pricing" type="partial_cargo_pricing_Type"/>
</xs:choice>
</xs:sequence>
</xs:choice>
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="panic_Type">
<xs:attribute name="desc" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="action_Type">
<xs:attribute name="target" type="action_target_type" use="required"/>
<xs:attribute name="type" type="action_type" use="required"/>
<xs:attribute name="value" type="xs:string" use="required"/>
</xs:complexType>
<xs:simpleType name="match_target_type">
<xs:restriction base="xs:string">
<xs:enumeration value="SRC_COUNTRY"/>
<xs:enumeration value="DST_COUNTRY"/>
<xs:enumeration value="SRC_ZIP"/>
<xs:enumeration value="DST_ZIP"/>
<xs:enumeration value="ROUTE"/>
<xs:enumeration value="TRUCKTYPE"/>
<xs:enumeration value="PRICE"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="match_type">
<xs:restriction base="xs:string">
<xs:enumeration value="EQUALS"/>
<xs:enumeration value="STARTS_WITH"/>
<xs:enumeration value="ENDS_WITH"/>
<xs:enumeration value="GREATER"/>
<xs:enumeration value="SMALLER"/>
<xs:enumeration value="PASSES"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="action_target_type">
<xs:restriction base="xs:string">
<xs:enumeration value="PRICE"/>
<xs:enumeration value="MIN_PRICE"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="action_type">
<xs:restriction base="xs:string">
<xs:enumeration value="ADD_ABS"/>
<xs:enumeration value="ADD_REL"/>
<xs:enumeration value="SET"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="evaluate_type">
<xs:restriction base="xs:string">
<xs:enumeration value="ALL"/>
<xs:enumeration value="UNTIL_FIRST_FIT"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="pricetable_Type">
<xs:choice maxOccurs="unbounded">
<xs:element name="pte" type="pricetable_entry_Type"/>
</xs:choice>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="pricing" type="pricing_type" use="required"/>
<xs:attribute name="entity_size" type="xs:double" use="required"/>
</xs:complexType>
<xs:complexType name="pricetable_entry_Type">
<xs:attribute name="count" type="xs:integer" use="required"/>
<xs:attribute name="percentage" type="xs:double" use="required"/>
</xs:complexType>
<xs:simpleType name="pricing_type">
<xs:restriction base="xs:string">
<xs:enumeration value="OVERALL_PERCENTAGE"/>
<xs:enumeration value="PER_ENTITY_PERCENTAGE"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="partial_cargo_pricing_Type">
<xs:attribute name="ldm_table" type="xs:string" use="required"/>
<xs:attribute name="pal_table" type="xs:string" use="required"/>
<xs:attribute name="weight_table" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="geoshape_Type">
<xs:choice>
<xs:element name="geocircle" type="geocircle_Type"/>
</xs:choice>
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="geocircle_Type">
<xs:sequence>
<xs:element name="center_lat" type="xs:double"/>
<xs:element name="center_lng" type="xs:double"/>
<xs:element name="radius" type="xs:double"/>
</xs:sequence>
</xs:complexType>
</xs:schema>



PriceTables werden benötigt, damit man, damit man für Teilladungen den prpzentuellen Teilladungspreis berechnen kann. So wird beispielsweise eine Preistabelle mit dem Namen LDM_A definiert. OVERALL_PERCENTAGE gibt an, dass es sich um absolute prozentuelle Werte handelt. Alternativ dazu könnte man PER_ENTITY_PERCENTAGE angeben. Dann würde der Preis pro Entity (also pro Ladungseinheit) berechnet werden. Dazu würde man das Attribut entity_size benötigen.

<pricetable id="LDM_A" pricing="OVERALL_PERCENTAGE" entity_size="1">
<pte percentage="16" count="1"></pte>


Als nächstes werden sogenannte GeoShapes definiert. Ein GeoShape ist ein Bereich auf der Landkarte. Man kann dann in den Regeln überprüfen, ob eine Route durch ein bestimmtes GeoShape verläuft. Ein GeoShape wird folgendermaßen definiert:

<geoshape id="INNSBRUCK">
<geocircle>
<center_lat>47.2496400077265</center_lat>
<center_lng>11.3962554931641</center_lng>
<radius>0.17204397565615925</radius>
</geocircle>
</geoshape>


Dabei handelt es sich um einen GeoCircle, d.h. einen Kreis auf der Landkarte mit gegebenem Mittelpunkt und Radius.
Um dem Ersteller einer solchen Regeldatei Schreibarbeit zu ersparen, habe ich das Konzept der ActionSets eingeführt. Dabei kann man eine Menge von Aktionen zu einem ActionSet zusammenfassen und in einer Regel einfach auf das ActionSet verweisen, anstatt alle darin enthaltenen Aktionen extra an die Regel anhängen zu müssen. Ein ActionSet wird wiefolgt definiert:

<actionset id="AS_150_80">
<action type="ADD_ABS" target="PRICE" value="150"/>
<action type="ADD_ABS" target="MIN_PRICE" value="80"/>
</actionset>

Dabei werden dem ActionSet zwei Aktionen zugeordnet. Man könnte jedoch rekursiv weitere ActionSets einbinden:

<actionset id="AS_400_350_W2_LDMA_PAL2">
<execute actionset="AS_400_350" />
<partial_cargo_pricing weight_table="WEIGHT_2" ldm_table="LDM_A" pal_table="PAL_2"/>
</actionset>

Hier wird ein ActionSet mit Hilfe des execute Elementes aufgerufen und zusätzlich eine partial_cargo_pricing Aktion ausgeführt. Eine solche Aktion bestimmt eine der oben definierten Preistabellen für jeden Teilladungstyp (Lademeter, Gewicht oder Paletten).

Danach folgen die einzelnen RuleSets mit ihren Kindregeln. Solche Kindregeln können weitere Kindregeln haben. Ein Beispiel für eine solche Regel sieht man im folgenden Listing:

<ruleset evaluate="UNTIL_FIRST_FIT" name="Main Actions">
<!-- Ladungen in die Schweiz -->
<rule match_target="DST_COUNTRY" match_type="EQUALS" match_value="CH">
<rule match_target="TRUCKTYPE" match_type="EQUALS" match_value="PLANE">
<execute actionset="AS_400_350_W2_LDMA_PAL2"/>
</rule>

...

Die erste Zeile definiert ein RuleSet mit dem Namen "Main Actions" und die Regeln werden so lange ausgeführt, bis man auf eine gültige Aktion trifft.Die erste Regel wird erfüllt, wenn es sich beim Zielland um die Schweiz handelt. Wird diese Regel nicht erfüllt, springt das Auswertungssystem auf die nächste Schwesterregel. Ist die Regel erfüllt, so wird überprüft ob es sich um ein Planenfahrzeug handelt. Ist diese Regel auch erfüllt, so wird das ActionSet AS_400_350_W2_LDMA_PAL2 ausgeführt. Wird sie nicht erfüllt, wird mit der nächsten Schwesterregel fortgefahren. Wird von diesen auch keine erfüllt, so wird eine Ebene höher mit der nächsten Schwesterregel weitergemacht usw. Es handelt sich also im Prinzip um einen DFS Algorithmus.
Interessant ist auch folgende Regel:

<rule match_target="ROUTE" match_type="PASSES" match_value="MONTBLANC">
<action type="ADD_ABS" value="1150" target="PRICE"/>
<partial_cargo_pricing weight_table="WEIGHT_3" pal_table="PAL_2" ldm_table="LDM_B"/>
</rule>


Hierbei wird überprüft, ob die Route über den Mont Blanc verläuft. ist dies der Fall, wird zum Preis für den Transport ein absoluter Betrag hinzugefügt.

Sodala, das wars dann auch wieder mit meinem kleinen Berechnungssystem. Ich finde dass mir so eine gute Alternative zwischen Flexibilität und Wiederverwendbarkeit gelungen ist. Ich hab unser Regelsystem jetzt mal von Hand zusammengestellt und bin auf eine XML Datei mit ca. 1500 Zeilen gekommen. Vielleicht werd ich mir in Zukunft in ein paar ruhigen Minuten einmal einen grafischen Editor programmieren.. wer weiß ?! :-)

Keine Kommentare:

Kommentar veröffentlichen