INHOUDSOPGAVE


Intro

In het koppelplatform kan je naast vaste waardes, vaste kolommen en hybride mapping ook de mogelijkheid selecteren om een waarde in een import via een formule te bepalen. Dit stelt je toe instaat om waardes conditioneel te importeren, ontbrekende waardes toe te voegen, waardes te converteren en meer.


XS Connect maakt voor de formules gebruik van CSharp (C#). 


Belangrijkste is dat: een formule altijd een String (of Null) moet teruggeven.


Selector 


In de formule heb je toegang tot een selector object, dit object heeft de Get method waarmee waardes uit het bestand gelezen kunnen worden. Welke waardes je toegang tot hebt hang af van het type importer en het bestandstype.

Op z'n minst kan je via een selector altijd de waardes ophalen die ook via een vaste kolom of hybride mapping beschikbaar zijn. 

Bij bestandstypes zoals CSV of Excel bestanden is het vrij eenvoudig. De importer zal de formule regel voor regel toepassen en bij iedere aanroep heeft de formule toegang tot informatie uit die regel. 

Door de Get method aan te roepen met de naam van een kolom kan je de waarde uit die kolom krijgen. Dus stel je hebt een spreadsheet met werknemer gegevens met de kolom "Achternaam" dan zou je de achternaam als volgt op kunnen halen in een formule:

 var achternaam = selector.Get("Achternaam");

Bij bestandstypes zoals XML of JSON heb je meer mogelijkheden. De converter gaat er standaard van uit dat het hoogste element in de hiërarchie in het bestand of een lijst is of een lijst bevat en zal de formule stap voor stap op ieder item in de lijst toepassen. Hier kan afgeweken worden via configuratie. 

Door de Get method aan te roepen met een pad naar een waarde in het bestand (relatief ten opzichte van het item in de lijst) kan je die waarde uit het bestand krijgen. Stel dit is je XML-bestand, neem aan dat importer geconfigureerd is om enkel Werknemer elementen te importen: 

<Import>
    <WerkgeverCode>code</WerkgeverCode>
    <Werknemer>
       <Achternaam>achternaam1</Achternaam>
        <Adres>
            <Straat>straat1</Straat>
            ...
        </Adres>
         ...
   </Werknemer>
   <Werknemer>
      <Achternaam>achternaam2</Achternaam>
        <Adres>
          <Straat>straat2</Straat>
           ...
         </Adres>
           ...
  </Werknemer>
 ...
</Import>

De importer zal de formule een voor een op ieder Werknemer element toepassen. Om de achternaam op te halen gebruik je dezelfde code als hierboven, want het achternaam is een direct onderdeel van de Werknemer 

var achternaam = selector.Get("Achternaam");

Om de straat op te halen moeten we in het pad aangeven dat deze in het Adres element zit: 

var straat = selector.Get("Adres/Straat");

Om de werkgever code op te halen moeten we omhoog in de hiërarchie van het bestand gaan, dit kan met:

var werkgeverCode = selector.Get("../WerkgeverCode");

Je kan meerder stappen dieper of omhoog in het bestand gaan. Een alternatieve manier is om bij de root / te beginnen en omhoog te gaan:

var werkgeverCode = selector.Get("/WerkgeverCode");

Waarde types

Standaard geeft de Get functie een string waarde terug (of null als er geen waarde gevonden kan worden, of als de waarde uit whitespace bestaat). Soms is het wenselijk om de waarde als een ander type te hebben omdat je bijvoorbeeld de waarde als nummer of datum wilt manipuleren. Dit kan je doen door de generic versie Get<> aan te roepen. 

Stel je wilt een verzuimpercentage als nummer ophalen dan zou je dit kunnen doen:

var percentage = selector.Get<double>("Percentage");

Het stuk tussen de <> specificeert welk type de waarde moet hebben. De selector heeft ingebouwde parser die de text uit het bestand probeert om te zetten naar het gespecificeerde type. Als de text in het bestand niet omgezet kan worden zal je een standaardwaarde terugkrijgen. Dit zijn de types die de selector kan teruggeven:

  • string 
  • int 
  • long 
  • double 
  • decimal 
  • DateTime 
  • bool 
  • byte 
  • Guid 

Ieder van deze types, behalve string, zou bij een lege of ontbrekende waarde in het bestand omgezet worden naar een standaardwaarde en niet naar null. Deze standaardwaarde is mogelijk niet te onderscheiden van een echte waarde in het bestand, zo zou je bij een int niet weten of het 0 is omdat er geen waarde in het bestand staat of omdat er 0 in het bestand staat. Mocht je dit onderscheid wel willen kunnen maken kan je een Nullable versie van het type gebruiken door een vraagteken achter het type te zetten, stel je wilt iets als nummer met een optionele leidinggevende id doen dan zou je die zo ophalen:

var id = selector.Get<int?>("LeidinggevendeId");

Let er wel op dat een nullable type niet het type zelf is. 

Het nullable type is een object met twee properties Value en HasValue. Als de waarde bestaat geeft HasValue true terug, zo niet dan geeft die false. Als die bestaat dan kan je de waarde vinden in Value, als de waarde niet bestaat en je probeert toch Value te gebruiken dan zal de formule een error opwerpen. Controleer dus altijd of de waarde bestaat als je die via Value probeert te gebruiken 


Node

Bij bestandstypes zoals XML of JSON is het mogelijk dat bij het verwerken van een bepaald gegeven meerdere onderdelen van een bestand relevant zijn. Bijvoorbeeld als je de leidinggevende van een dienstverband aan het verwerken bent kan het zijn dat je gegevens uit een leidinggevende element, dienstverband element en werknemer element nodig hebt. Daarom dat bij sommige gegevens meerdere onderdelen beschikbaar zijn via de selector door een node te specificeren in de Get aanroep. Stel je wilt een personeelsnummer van de werknemer hebben tijdens een dienstverband import dan zou je dit kunnen doen:

var personeelsnummer = selector.Get("PersoneelsNummer", "werknemer");

Welke nodes exact beschikbaar zijn bij welk gegeven varieert. In het algemeen komt dit voor als bij het verwerken van een bepaald element (bijvoorbeeld werknemer) er ook meerder dieper gelegen elementen verwerkt kunnen worden (bijvoorbeeld dienstverbanden, adressen, contactpersonen). 


Op het moment van schrijven (30/05/2023) is dit enkel relevant voor de ConverterDvXml. Hierbij heb je voor de volgende gegevens toegang tot deze nodes:

  • Werknemer gegevens: root en werknemer nodes.
  • Dienstverband gegevens: root, werknemer en dienstverband nodes.
  • Contract gegevens: root, werknemer en contract nodes.
  • Contract mutatie gegevens: root, werknemer, contract en contractmutatie nodes.

Voorbeelden

Vaste waarde

Deze zal je in de praktijk nooit gebruiken omdat deze identiek is aan een vaste waarde mapping. Hier wordt een vaste waarde terug gegeven: 

return "een vaste waarde";

Stel dat je een random guid wil genereren dan zal je het resultaat expliciet in een string moeten veranderen:

return Guid.NewGuid().ToString();

Vaste kolom

Deze zal je ook in de praktijk niet gebruiken omdat deze identiek is aan een vaste kolom mapping. Hier wordt de kolom (of JSON property, of xml tag) "BSN" terug gegeven: 

return selector.Get("BSN");

Vaste kolommen combineren

Deze zal je ook niet in de praktijk gebruiken omdat deze identiek is aan een hybride mapping. Hier worden een leidinggevende id en afdeling id gecombineerd (om bijvoorbeeld een unieke code te genereren) 

return selector.Get("AfdelingId") + "_" + selector.Get("LeidinggevendeId");

Een alternatieve manier om dit te doen is door een string interpolation te gebruiken: 

return $"{selector.Get("AfdelingId")}_{selector.Get("LeidinggevendeId")}";

Hiermee kan je de waardes die in de string moeten komen direct in de string zetten. Let op de $ en { }.


Vaste kolommen met optionele waardes combineren 

Stel je wilt meerdere waardes combineren maar enkel als alle waardes bestaan. 

Bijvoorbeeld een achternaam met optionele voornaam:

var voornaam = selector.Get("Voornaam");
if (voornaam != null) 
{
 return selector.Get("Achternaam") + ", " + voornaam;
}
else 
{
 return selector.Get("Achternaam");
}

Als het aantal waardes dat je wilt combineren groter wordt kan deze aanpak onhandig worden omdat je voor te veel combinaties if statements moet schrijven. 

Dit kan bijvoorbeeld door een StringBuilder te gebruiken en daarmee stap voor stap het resultaat op te bouwen:

var sb = new System.Text.StringBuilder();
sb.Append(selector.Get("Achternaam"));
var voornaam = selector.Get("Voornaam");
if (voornaam != null) {
   sb.Append($", ");
   sb.Append(voornaam);
   var tussenvoegsel = selector.Get("Tussenvoegsel");
   if (tussenvoegsel != null) {
     sb.Append(" ");
     sb.Append(tussenvoegsel);
   }
}
return sb.ToString();

Of door een lijst van waardes op te stellen en die te combineren: 

(Hierbij zouden alle notities achter elkaar gezet worden met witregels ertussen.) 

return string.Join("\n\n",
 selector.Get("Notitie1"),
 selector.Get("Notitie2"),
 selector.Get("Notitie3"),
 selector.Get("Notitie4"),
 selector.Get("Notitie5")
);

Voorwaardelijke uit bepaalde kolommen waardes halen 

Stel je wilt onder sommige omstandigheden een bepaald gegeven uit kolom A halen en onder andere omstandigheden uit kolom B dan zou je zoiets kunnen doen:

if (selector.Get("VerzuimSoort") == "A") {
     return selector.Get("KolomA");
} 
else 
{
     return selector.Get("KolomB");
}

Als er veel mogelijke gevallen zijn is het handig om een switch te gebruiken, hierin zal de formule de opgegeven waarde vergelijken met iedere: 

switch (selector.Get("VerzuimSoort")) {
 case "A": return selector.Get("KolomA");
 case "B": return selector.Get("KolomB");
 case "C": return selector.Get("KolomC");
 case "D": return selector.Get("KolomD");
 case "E": return selector.Get("KolomE");
 default: return null;
}

Let op de default case. Iedere formule moet uiteindelijk expliciet iets teruggeven. In plaats van een default zou je ook een return statement aan het einde van de formule te zetten. 

In theorie het zelfs mogelijk om de formule de naam van de kolom te laten bepalen:

return selector.Get("Kolom" + selector.Get("VerzuimSoort"));

Gebruik dit alleen als je zeker weet welke mogelijke waardes er in het import bestand gebruikt worden/ kunnen staan.


Terugval naar een vaste waarde

Stel je bent een gegeven aan het verwerken dat verplicht is in XS maar optioneel voor komt in het bestand. Dan zou je op deze manier een terugval naar een vaste waarden kunnen doen: 

var functie = selector.Get("Functie");
  if (functie != null)
    return functie;
  else
    return "Onbekend";

(Methods als String.IsNullOrEmpty zijn hier niet nodig omdat de selector een lege of whitespace string altijd als null teruggeeft.) 

Dit kan beknopter door gebruik te maken van de null-coalescing operator:

return selector.Get("Functie") ?? "Onbekend";

Stel je bent een startdatum aan het importeren, deze kan ontbreken en moet terugvallen op vandaag dan zou je dit als volgt kunnen doen: 

var start = selector.Get("Start"); if (start.HasValue) return start.ToString(); else return DateTime.Today.ToString() 

var start = selector.Get<DateTime?>("Start");
   if (start.HasValue)
     return start.ToString();
   else
     return DateTime.Today.ToString()

Let hier op het gebruik van een nullable waarde en de HasValue property. 

Hier zou je ook de null-coalescing operator kunnen gebruiken:

 

return (selector.Get<DateTime?>("Start") ?? DateTime.Today).ToString();

Terugval naar andere kolom en naar vaste waarde

Stel je wilt terugval naar een of meerdere kolommen en uiteindelijk een vaste waarde. Dan zou je zoiets kunnen doen:

return selector.Get("Functie3") ?? selector.Get("Functie2") ?? selector.Get("Functie1") ?? "Onbekend";

Hier zal de formule de voorkeur hebben voor de waarde van Functie3, dan Functie2, dan Functie1 en uiteindelijk de vaste waarde. 

Als je geen uiteindelijke terugval naar een vaste waarde wilt dan kan je die ook weglaten (inclusief de laatste ??).


Waarde uit kolom via mapping omzetten - Switch

Stel je wilt de waarde in het bestand bevat een code die vertaald moet worden naar iets dat XS kan importeren, dan zou je zoiets kunnen doen: 

var protocol = selector.Get("Protocol");
switch (protocol) 
{ 
 case "Z":
 case "Ziekte":
 case "Ziekte, zwaar": return "Ziek";
 case "Zwangerschap": return "Zwanger";
 default: return protocol;
}

De case regels waar niets achter staan zullen de waarde van de case direct er onder doorgeven. Dus in dit geval zullen Z en Ziekte allebei naar Ziek omgezet worden. 


Stel je hebt de gemapte waarde later nodig in de formule dan kan je de waarde aan een variabele toewijzen in de switch: 

string protocol;
switch (selector.Get("Protocol"))
{
 case "Z":
 case "Ziekte":
 case "Ziekte, zwaar": protocol = "Ziek"; break;
 case "Zwangerschap": protocol = "Zwanger"; break;
 default: protocol = "Onbekend"; break;
}

Let op het gebruik van break. Dit is nodig een switch alles na de eerst matchende case probeert uit te voeren inclusief regels bij volgende cases, de break voorkomt dit. Let ook op het gebruik van string in het declareren van de variabele. Meestal laten we de compiler automatisch het type bepalen via var maar omdat we hier protocol niet direct een waarde geven kan dit niet. 

Het is mogelijk om ook gehele getallen in een switch statement gebruiken. Al kan het ook handiger zijn om if statements te gebruiken als je hele series van waardes op dezelfde waarde wilt mappen. Zoals dit bijvoorbeeld: 

var afdelingId = selector.Get<int>("AfdelingId");
if (afdelingId <= 0)
    return null;
else if (afdelingId <= 5)
    return "executive";
else if (afdelingId <= 25)
    return "management";
else if (afdelingId <= 100)
    return "retail";
else
    return "other";

Hier zal de formule een voor een de if statements langs gaan totdat er eentje gevonden is die waar is en die wordt dan uitgevoerd, met uiteindelijke terugval op onderste else. Hierdoor zal 26 t/m 100 gemapt worden op retail omdat alles onder de 26 al eerder afgevangen zou worden. 


Verzuimpercentage berekenen

Stel het bestand bevat een hersteldpercentage en je wilt deze omrekenen omdat XS een verzuimpercentage verwacht (in de praktijk bestaat een setting voor om hier mee rekening te houden): 

return (100.0 - selector.Get<double>("HersteldPercentage")).ToString();

Het resultaat van de berekening moet expliciet in een string veranderd worden.

De bovestaande formule zal 100 teruggeven als er geen hersteldpercentage gevonden kan worden omdat een niet bestaande double type standaard 0 is. Als hier van afgeweken moet worden zal je een nullabe double moeten gebruiken. 

Stel er moet bij ontbreken van hersteldpercentage teruggevallen worden op volledig hersteld: 

return (100.0 - (selector.Get<double?>("HersteldPercentage") ?? 100.0)).ToString();

Of bij ontbreken moet er niets teruggegeven worden: 

var percentage = selector.Get<double?>("HersteldPercentage");
if (percentage.HasValue)
    return (100.0 - precentage.Value).ToString();
return null;

FTE-factor berekenen

Stel je wilt de fte factor berekenen op basis van het aantal uren voor een dienstverband en een optioneel uren norm dat alleen gegeven wordt als deze afwijkt van 40. Dan zou je dat op deze manier kunnen doen: 

return (selector.Get<double>("Uren") / (selector.Get<double?>("NormUren") ?? 40.0)).ToString();

De types zijn van belang hier. Een berekening op basis van gehele getallen zal altijd een geheel getal op leveren ook al zou het resultaat een fractie moeten zijn daarom dat je expliciet double moet gebruiken als je ook daadwerkelijk een niet geheel resultaat wilt hebben. 

Omdat de normuren optioneel zijn worden ze als nullable double opgehaald. Null coalesing kan je dan gebruiken om er alsnog een niet nullable double van te maken.


Datum verschuiven

Stel hebt een eerste werkdag maar hebt een laatste ziektedag nodig, dan zou je deze formule kunnen gebruiken: 

return selector.Get<DateTime>("EersteWerkdag").AddDays(-1).ToString();

Stel de datum in kwestie is optioneel dan moet je de formule zo schrijven, omdat bij ontbreken dDateTime terugvalt op de standaard waarde 1900-01-01 wat de laagst mogelijke datum is bij dit type: 

return selector.Get<DateTime?>("EersteWerkdag")?.AddDays(-1).ToString();

Door een nullable DateTime zal die null teruggeven als de waarde ontbreekt (en niet 1900-01-01). 

Echter kan je geen method aanroepen op een object dat null is, daarom dat er een ? voor AddDays staat. Hierdoor zal de formule eerst controleren of de waarde null is en in dat geval de AddDays method niet aanroepen maar direct null teruggeven.  


Text manipulatie

Er zijn allerlei manieren om text te manipuleren. Let op de vraagtekens, deze zijn belangrijk omdat Get null terug kan geven en op null kan je de methods niet aanroepen. Tekst verwijderen, hier wordt "TelNr: " verwijdert: 

return selector.Get("TelefoonNr")?.Replace("TelNr: ", "");

Tekst vervangen, hier wordt "@oud.nl" vervangen door "@nieuw.nl": 


return selector.Get("Email")?.Replace("@oud.nl", "@nieuw.nl");

Lijst opsplitsen, hier wordt de laatste regel uit een lijst gepakt: 

var lines = selector.Get("Woonplaatsen")?.Split('\n');
if (lines?.Length > 0)
{
   return lines[lines.Length - 1];
}
return null;

Let op lijsten in C# beginnen bij 0, daarom dat er 1 van de lengte afgetrokken wordt om de positie van het laatste element te krijgen. 

Onderdeel uit tekst halen, hier wordt het gedeelte tussen haakjes terug gegeven: 

var tekst = selector.Get("Adres");
var begin = tekst.IndexOf("(") + 1;
var einde = tekst.IndexOf(")");
if (begin > -1 && einde > -1)
{
 return tekst.Substring(begin, einde - begin);
}
else if (begin > -1)
{
 return tekst.Substring(begin);
}
else if (einde > -1)
{
 return tekst.Substring(0, einde);
}
return tekst;

Deze is wat ingewikkelder omdat er rekening gehouden wordt met dat er mogelijk maar 1 haakje gevonden wordt. De Substring method verwacht een lengte als tweede parameter. 


Boolean tests

Bij sommige velden moet een boolean (true of false) waarde terug gegeven worden echter moet moet dit in de formule ook als string teruggegeven worden. De makkelijke manier om dit af te dwingen is door de waarde expliciet naar een string om te zetten. 


Test of een id lager dan 100 is:

return (selector.Get<int>("id") < 100).ToString();

Test of een waarde een specifieke tekst bevat: 

return (selector.Get("Functie")?.IndexOf("temp") > -1).ToString();

Test of een waarde met een specifiek tekst begint: 

return selector.Get("Code")?.StartsWith("L").ToString();

Test of een datum in de toekomst ligt: 

return (selector.Get<DateTime>("Start") > DateTime.Now).ToString();

Test of een datum voor gisteren ligt:

return (selector.Get<DateTime>("Einde") < DateTime.Today.AddDays(-1)).ToString();

Test of een periode langer dan 100 dagen is: 

return (((selector.Get<DateTime?>("Einde") ?? DateTime.Today) - selector.Get<DateTime>("Start") ?? DateTime.Today)))

Test of een waarde voor komt in een lijst met opties: 

var opties = new System.Collections.Generic.List<string> { "optie1", "optie2", "optie3" }; return
opties.Contains(selector.Get("Optie")).ToString();

Technisch zou de lijst met opties ook uit het bestand kunnen komen. 


Map naam baseren op eerste letter van werkgever naam

Bij werkgever import is er de optie om werkgevers in mappen te zetten en soms zijn er zo veel werkgevers dat ze in op basis van eerste letter van hun naam gegroepeerd moeten worden. Dus alle werkgevers die met a beginnen in een map a, met b in map b, etc. 

In dat geval zal je een formule moeten gebruiken om de correcte mapnaam af te lijden van de werkgever naam. Deze formule geeft de eerste letter van een naam terug (zonder accentjes), of "0-9" als het een nummer is, of "Overige" als het iets anders is en null als er geen naam is:

var naam = selector.Get("WerkgeverNaam")?.Trim();
if (String.IsNullOrEmpty(naam))
 return null;

var letter = naam.Substring(0, 1).ToUpper().Normalize(System.Text.NormalizationForm.FormD)[0];
if (Char.IsLetter(letter))
{
 return letter.ToString();
}
else if (Char.IsDigit(letter))
{
 return "0-9";
}
else
{
 return "Overige";
}

De Normalize method wordt gebruikt om de accentjes weg te halen, deze method veranderd een "È" in de decompositie "E`" waarna we opnieuw de eerste letter pakken. De Char methods verwacht een char type en een manier om een char uit een string te halen is via de index aanroep [].