Source of file SepaUtilities.php

Size: 55,260 Bytes - Last Modified: 2016-10-16T02:22:09+02:00

D:/Cloud/Dropbox/Git/SepaUtilities/src/SepaUtilities.php

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168
<?php
/**
 * SepaUtilities
 *
 * @license   GNU LGPL v3.0 - For details have a look at the LICENSE file
 * @copyright ©2015 Alexander Schickedanz
 * @link      https://github.com/AbcAeffchen/Sephpa
 *
 * @author  Alexander Schickedanz <abcaeffchen@gmail.com>
 */
namespace AbcAeffchen\SepaUtilities;

/**
 * Useful methods to validate an sanitize input used in SEPA files
 */
class SepaUtilities
{
    // credit transfers version
    const SEPA_PAIN_001_002_03      = 100203;
    const SEPA_PAIN_001_003_03      = 100303;
    const SEPA_PAIN_001_001_03      = 100103;
    const SEPA_PAIN_001_001_03_GBIC = 1001031;
    // direct debit versions
    const SEPA_PAIN_008_002_02      = 800202;
    const SEPA_PAIN_008_003_02      = 800302;
    const SEPA_PAIN_008_001_02      = 800102;
    const SEPA_PAIN_008_001_02_GBIC = 8001021;

    const HTML_PATTERN_IBAN = '([a-zA-Z]\s*){2}([0-9]\s?){2}\s*([a-zA-Z0-9]\s*){1,30}';
    const HTML_PATTERN_BIC = '([a-zA-Z]\s*){6}[a-zA-Z2-9]\s*[a-nA-Np-zP-Z0-9]\s*(([A-Z0-9]\s*){3}){0,1}';

    const PATTERN_IBAN = '[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}';
    const PATTERN_BIC  = '[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3}){0,1}';
    /**
     * equates to RestrictedPersonIdentifierSEPA
     */
    const PATTERN_CREDITOR_IDENTIFIER  = '[a-zA-Z]{2,2}[0-9]{2,2}([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|\']){3,3}([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|\']){1,28}';
    /**
     * used for Names, etc.
     */
    const PATTERN_SHORT_TEXT  = '[a-zA-Z0-9/\-?:().,\'+\s]{0,70}';
    /**
     * used for remittance information
     */
    const PATTERN_LONG_TEXT  = '[a-zA-Z0-9/\-?:().,\'+\s]{0,140}';
    /**
     * Used for Message-, Payment- and Transfer-IDs (since 2016 also for Mandate-ID)
     */
    const PATTERN_RESTRICTED_IDENTIFICATION_SEPA1 = '([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|\'|\s]){1,35}';
    /**
     * Used for Mandate-ID
     */
    const PATTERN_RESTRICTED_IDENTIFICATION_SEPA2 = '([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|\']){1,35}';
    /**
     * This is just for compatibility to v1.1.*
     */
    const PATTERN_MANDATE_ID = self::PATTERN_RESTRICTED_IDENTIFICATION_SEPA2;

    const FLAG_ALT_REPLACEMENT_GERMAN = 1;      // 1 << 0
    const FLAG_NO_REPLACEMENT_GERMAN  = 32768;  // 1 << 15

    /**
     * first direct debit
     */
    const SEQUENCE_TYPE_FIRST     = 'FRST';
    /**
     * recurring direct debit
     */
    const SEQUENCE_TYPE_RECURRING = 'RCUR';
    /**
     * one time direct debit
     */
    const SEQUENCE_TYPE_ONCE      = 'OOFF';
    /**
     * final direct debit
     */
    const SEQUENCE_TYPE_FINAL     = 'FNAL';
    /**
     * normal direct debit
     */
    const LOCAL_INSTRUMENT_CORE_DIRECT_DEBIT     = 'CORE';
    /**
     * urgent direct debit
     */
    const LOCAL_INSTRUMENT_CORE_DIRECT_DEBIT_D_1 = 'COR1';
    /**
     * business direct debit
     */
    const LOCAL_INSTRUMENT_BUSINESS_2_BUSINESS   = 'B2B';
    /**
     * @type int BIC_REQUIRED_THRESHOLD Until 2016-01-31 (incl.) the BIC is required for international
     *           payment transactions
     */
    const BIC_REQUIRED_THRESHOLD = 20160131;

    private static $ibanPatterns = array('EG' => 'EG[0-9]{2}[0-9A-Z]{23}',
                                         'AL' => 'AL[0-9]{10}[0-9A-Z]{16}',
                                         'DZ' => 'DZ[0-9]{2}[0-9A-Z]{20}',
                                         'AD' => 'AD[0-9]{10}[0-9A-Z]{12}',
                                         'AO' => 'AL[0-9]{2}[0-9A-Z]{21}',
                                         'AZ' => 'AZ[0-9]{2}[0-9A-Z]{24}',
                                         'BH' => 'AL[0-9]{2}[0-9A-Z]{18}',
                                         'BE' => 'BE[0-9]{14}',
                                         'BJ' => 'BJ[0-9]{2}[0-9A-Z]{24}',
                                         'BA' => 'BA[0-9]{18}',
                                         'BR' => 'BR[0-9]{2}[0-9A-Z]{25}',
                                         'VG' => 'VG[0-9]{2}[0-9A-Z]{20}',
                                         'BG' => 'BG[0-9]{2}[A-Z]{4}[0-9]{6}[0-9A-Z]{8}',
                                         'BF' => 'BF[0-9]{2}[0-9A-Z]{23}',
                                         'BI' => 'BI[0-9]{2}[0-9A-Z]{12}',
                                         'CR' => 'CR[0-9]{2}[0-9A-Z]{17}',
                                         'CI' => 'CI[0-9]{2}[0-9A-Z]{24}',
                                         'DK' => 'DK[0-9]{16}',
                                         'DE' => 'DE[0-9]{20}',
                                         'DO' => 'DO[0-9]{2}[0-9A-Z]{24}',
                                         'EE' => 'EE[0-9]{18}',
                                         'FO' => 'FO[0-9]{16}',
                                         'FI' => 'FI[0-9]{16}',
                                         'FR' => 'FR[0-9]{2}[0-9A-Z]{23}',
                                         'GA' => 'GA[0-9]{2}[0-9A-Z]{23}',
                                         'GE' => 'GE[0-9]{2}[A-Z]{2}[0-9A-Z]{16}',
                                         'GI' => 'GI[0-9]{2}[A-Z]{4}[0-9]{15}',
                                         'GR' => 'GR[0-9]{9}[0-9A-Z]{16}',
                                         'GL' => 'GL[0-9]{16}',
                                         'GT' => 'GT[0-9]{2}[0-9A-Z]{24}',
                                         'IR' => 'IR[0-9]{2}[0-9A-Z]{22}',
                                         'IE' => 'IE[0-9]{2}[A-Z]{4}[0-9]{14}',
                                         'IS' => 'IS[0-9]{24}',
                                         'IL' => 'IL[0-9]{21}',
                                         'IT' => 'IT[0-9]{2}[A-Z]{1}[0-9]{10}[0-9A-Z]{12}',
                                         'JO' => 'JO[0-9]{2}[0-9A-Z]{26}',
                                         'CM' => 'CM[0-9]{2}[0-9A-Z]{23}',
                                         'CV' => 'CV[0-9]{2}[0-9A-Z]{21}',
                                         'KZ' => 'KZ[0-9]{5}[0-9A-Z]{13}',
                                         'QA' => 'QA[0-9]{2}[0-9A-Z]{25}',
                                         'CG' => 'CG[0-9]{2}[0-9A-Z]{23}',
                                         'KS' => 'KS[0-9]{2}[0-9A-Z]{16}',  // todo: This should be the IBAN format for Kosovo. Is this correct?
                                         'HR' => 'HR[0-9]{19}',
                                         'KW' => 'KW[0-9]{2}[A-Z]{4}[0-9A-Z]{22}',
                                         'LV' => 'LV[0-9]{2}[A-Z]{4}[0-9A-Z]{13}',
                                         'LB' => 'LB[0-9]{6}[0-9A-Z]{20}',
                                         'LI' => 'LI[0-9]{7}[0-9A-Z]{12}',
                                         'LT' => 'LT[0-9]{18}',
                                         'LU' => 'LU[0-9]{5}[0-9A-Z]{13}',
                                         'MG' => 'MG[0-9]{2}[0-9A-Z]{23}',
                                         'ML' => 'ML[0-9]{2}[0-9A-Z]{24}',
                                         'MT' => 'MT[0-9]{2}[A-Z]{4}[0-9]{5}[0-9A-Z]{18}',
                                         'MR' => 'MR[0-9]{25}',
                                         'MU' => 'MU[0-9]{2}[0-9A-Z]{23}[A-Z]{3}',
                                         'MK' => 'MK[0-9]{5}[0-9A-Z]{10}[0-9]{2}',
                                         'MD' => 'MD[0-9]{2}[0-9A-Z]{20}',
                                         'MC' => 'MC[0-9]{12}[0-9A-Z]{11}[0-9]{2}',
                                         'ME' => 'ME[0-9]{20}',
                                         'MZ' => 'MZ[0-9]{2}[0-9A-Z]{21}',
                                         'NL' => 'NL[0-9]{2}[A-Z]{4}[0-9]{10}',
                                         'NO' => 'NO[0-9]{13}',
                                         'AT' => 'AT[0-9]{18}',
                                         'TL' => 'TL[0-9]{2}[0-9A-Z]{16}',
                                         'PK' => 'PK[0-9]{2}[0-9A-Z]{20}',
                                         'PS' => 'PS[0-9]{2}[0-9A-Z]{25}',
                                         'PL' => 'PL[0-9]{26}',
                                         'PT' => 'PT[0-9]{23}',
                                         'RO' => 'RO[0-9]{2}[A-Z]{4}[0-9A-Z]{16}',
                                         'SM' => 'SM[0-9]{2}[A-Z]{1}[0-9]{10}[0-9A-Z]{12}',
                                         'ST' => 'ST[0-9]{2}[0-9A-Z]{21}',
                                         'SA' => 'SA[0-9]{4}[0-9A-Z]{18}',
                                         'SE' => 'SE[0-9]{22}',
                                         'CH' => 'CH[0-9]{2}[0-9]{5}[0-9A-Z]{12}',
                                         'SN' => 'SN[0-9]{2}[0-9A-Z]{24}',
                                         'RS' => 'RS[0-9]{20}',
                                         'SK' => 'SK[0-9]{22}',
                                         'SI' => 'SI[0-9]{17}',
                                         'ES' => 'ES[0-9]{22}',
                                         'CZ' => 'CZ[0-9]{22}',
                                         'TN' => 'TN[0-9]{22}',
                                         'TR' => 'TR[0-9]{7}[0-9A-Z]{17}',
                                         'HU' => 'HU[0-9]{26}',
                                         'AE' => 'AE[0-9]{2}[0-9A-Z]{19}',
                                         'GB' => 'GB[0-9]{2}[A-Z]{4}[0-9]{14}',
                                         'CY' => 'CY[0-9]{10}[0-9A-Z]{16}',
                                         'CF' => 'CF[0-9]{2}[0-9A-Z]{23}');

    private static $alphabet = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
                                     'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
                                     'U', 'V', 'W', 'X', 'Y', 'Z');

    private static $alphabetValues = array( 10,  11,  12,  13,  14,  15,  16,  17,  18,  19,
                                            20,  21,  22,  23,  24,  25,  26,  27,  28,  29,
                                            30,  31,  32,  33,  34,  35);

    private static $mod97Values = array( 1, 10,  3, 30,  9, 90, 27, 76, 81, 34, 49,  5, 50, 15, 53, 45, 62, 38,
                                        89, 17, 73, 51, 25, 56, 75, 71, 31, 19, 93, 57, 85, 74, 61, 28, 86,
                                        84, 64, 58, 95, 77, 91, 37, 79, 14, 43, 42, 32, 29, 96, 87, 94, 67,
                                        88,  7, 70, 21, 16, 63, 48, 92, 47, 82, 44, 52, 35, 59,  8, 80, 24);

    private static $specialChars            = array(';','[','\\',']','^','_','`', '{','|','}','~','¿','À','Á','Â','Ã','Ä','Å'
,'Æ','Ç','È','É','Ê','Ë','Ì','Í','Î','Ï','Ð','Ñ','Ò','Ó','Ô','Õ','Ö','Ø','Ù','Ú','Û','Ü','Ý','Þ','ß','à','á','â','ã','ä','å','æ','ç','è','é','ê','ë','ì','í','î','ï','ð','ñ','ò','ó','ô','õ','ö','ø','ù','ú','û','ü','ý','þ','ÿ','Ā','ā','Ă','ă','Ą','ą','Ć','ć','Ĉ','ĉ','Ċ','ċ','Č','č','Ď','ď','Đ','đ','Ē','ē','Ĕ','ĕ','Ė','ė','Ę','ę','Ě','ě','Ĝ','ĝ','Ğ','ğ','Ġ','ġ','Ģ','ģ','Ĥ','ĥ','Ħ','ħ','Ĩ','ĩ','Ī','ī','Ĭ','ĭ','Į','į','İ','ı','IJ','ij','Ĵ','ĵ','Ķ','ķ','ĸ','Ĺ','ĺ','Ļ','ļ','Ľ','ľ','Ŀ','ŀ','Ł','ł','Ń','ń','Ņ','ņ','Ň','ň','Ő','ő','Œ','œ','Ŕ','ŕ','Ŗ','ŗ','Ř','ř','Ś','ś','Ŝ','ŝ','Ş','ş','Š','š','Ţ','ţ','Ť','ť','Ŧ','ŧ','Ũ','ũ','Ū','ū','Ŭ','ŭ','Ů','ů','Ű','ű','Ų','ų','Ŵ','ŵ','Ŷ','ŷ','Ÿ','Ź','ź','Ż','ż','Ž','ž','Ș','ș','Ț','ț','Ά','Έ','Ή','Ί','Ό','Ύ','Ώ','ΐ','Α','Β','Γ','Δ','Ε','Ζ','Η','Θ' ,'Ι','Κ','Λ','Μ','Ν','Ξ','Ο','Π','Ρ','Σ','Τ','Υ','Φ','Χ', 'Ψ', 'Ω','Ϊ','Ϋ','ά','έ','ή','ί','ΰ','α','β','γ','δ','ε','ζ','η','θ', 'ι','κ','λ','μ','ν','ξ','ο','π','ρ','ς','σ','τ','υ','φ','χ', 'ψ', 'ω','ϊ','ϋ','ό','ύ','ώ','А','Б','В','Г','Д','Е','Ж', 'З','И','Й','К','Л','М','Н','О','П','Р','С','Т','У','Ф','Х','Ц', 'Ч', 'Ш', 'Щ',  'Ъ','Ь','Ю', 'Я', 'а','б','в','г','д','е','ж', 'з','и','й','к','л','м','н','о','п','р','с','т','у','ф','х','ц', 'ч', 'ш', 'щ',  'ъ','ь','ю', 'я', '€');    private static $specialCharsReplacement = array(';' => ',', '[' => '(', '\\' => '/', ']' => ')', '^' => '.', '_' => '-', '`' => '\'', '{' => '(', '|' => '/', '}' => ')', '~' => '-', '¿' => '?', 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'A', 'Ç' => 'C', 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', 'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ý' => 'Y', 'Þ' => 'T', 'ß' => 's', 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'a', 'ç' => 'c', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ý' => 'y', 'þ' => 't', 'ÿ' => 'y', 'Ā' => 'A', 'ā' => 'a', 'Ă' => 'A', 'ă' => 'a', 'Ą' => 'A', 'ą' => 'a', 'Ć' => 'C', 'ć' => 'c', 'Ĉ' => 'C', 'ĉ' => 'c', 'Ċ' => 'C', 'ċ' => 'c', 'Č' => 'C', 'č' => 'c', 'Ď' => 'D', 'ď' => 'd', 'Đ' => 'D', 'đ' => 'd', 'Ē' => 'E', 'ē' => 'e', 'Ĕ' => 'E', 'ĕ' => 'e', 'Ė' => 'E', 'ė' => 'e', 'Ę' => 'E', 'ę' => 'e', 'Ě' => 'E', 'ě' => 'e', 'Ĝ' => 'G', 'ĝ' => 'g', 'Ğ' => 'G', 'ğ' => 'g', 'Ġ' => 'G', 'ġ' => 'g', 'Ģ' => 'G', 'ģ' => 'g', 'Ĥ' => 'H', 'ĥ' => 'h', 'Ħ' => 'H', 'ħ' => 'h', 'Ĩ' => 'I', 'ĩ' => 'i', 'Ī' => 'I', 'ī' => 'i', 'Ĭ' => 'I', 'ĭ' => 'i', 'Į' => 'I', 'į' => 'i', 'İ' => 'I', 'ı' => 'i', 'IJ' => 'I', 'ij' => 'i', 'Ĵ' => 'J', 'ĵ' => 'j', 'Ķ' => 'K', 'ķ' => 'k', 'ĸ' => '.', 'Ĺ' => 'L', 'ĺ' => 'l', 'Ļ' => 'L', 'ļ' => 'l', 'Ľ' => 'L', 'ľ' => 'l', 'Ŀ' => 'L', 'ŀ' => 'l', 'Ł' => 'L', 'ł' => 'l', 'Ń' => 'N', 'ń' => 'n', 'Ņ' => 'N', 'ņ' => 'n', 'Ň' => 'N', 'ň' => 'n', 'Ő' => 'O', 'ő' => 'o', 'Œ' => 'O', 'œ' => 'o', 'Ŕ' => 'R', 'ŕ' => 'r', 'Ŗ' => 'R', 'ŗ' => 'r', 'Ř' => 'R', 'ř' => 'r', 'Ś' => 'S', 'ś' => 's', 'Ŝ' => 'S', 'ŝ' => 's', 'Ş' => 'S', 'ş' => 's', 'Š' => 'S', 'š' => 's', 'Ţ' => 'T', 'ţ' => 't', 'Ť' => 'T', 'ť' => 't', 'Ŧ' => 'T', 'ŧ' => 't', 'Ũ' => 'U', 'ũ' => 'u', 'Ū' => 'U', 'ū' => 'u', 'Ŭ' => 'U', 'ŭ' => 'u', 'Ů' => 'U', 'ů' => 'u', 'Ű' => 'U', 'ű' => 'u', 'Ų' => 'U', 'ų' => 'u', 'Ŵ' => 'W', 'ŵ' => 'w', 'Ŷ' => 'Y', 'ŷ' => 'y', 'Ÿ' => 'Y', 'Ź' => 'Z', 'ź' => 'z', 'Ż' => 'Z', 'ż' => 'z', 'Ž' => 'Z', 'ž' => 'z', 'Ș' => 'S', 'ș' => 's', 'Ț' => 'T', 'ț' => 't', 'Ά' => 'A', 'Έ' => 'E', 'Ή' => 'I', 'Ί' => 'I', 'Ό' => 'O', 'Ύ' => 'Y', 'Ώ' => 'O', 'ΐ' => 'i', 'Α' => 'A', 'Β' => 'V', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'I', 'Θ' => 'TH', 'Ι' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => 'X', 'Ο' => 'O', 'Π' => 'P', 'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'F', 'Χ' => 'CH', 'Ψ' => 'PS', 'Ω' => 'O', 'Ϊ' => 'I', 'Ϋ' => 'Y', 'ά' => 'a', 'έ' => 'e', 'ή' => 'i', 'ί' => 'i', 'ΰ' => 'y', 'α' => 'a', 'β' => 'v', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'i', 'θ' => 'th', 'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => 'x', 'ο' => 'o', 'π' => 'p', 'ρ' => 'r', 'ς' => 's', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'χ' => 'ch', 'ψ' => 'ps', 'ω' => 'o', 'ϊ' => 'i', 'ϋ' => 'y', 'ό' => 'o', 'ύ' => 'y', 'ώ' => 'o', 'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ж' => 'ZH', 'З' => 'Z', 'И' => 'I', 'Й' => 'Y', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O', 'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'TS', 'Ч' => 'CH', 'Ш' => 'SH', 'Щ' => 'SHT', 'Ъ' => 'A', 'Ь' => 'Y', 'Ю' => 'YU', 'Я' => 'YA', 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ж' => 'zh', 'з' => 'z', 'и' => 'i', 'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'ts', 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sht', 'ъ' => 'a', 'ь' => 'y', 'ю' => 'yu', 'я' => 'ya', '€' => 'E');

    /**
     * @type array $bicIbanCountryCodeExceptions IBAN country code => array of valid BIC country codes
     */
    private static $bicIbanCountryCodeExceptions = array('FR' => array('GF', 'GP', 'MQ', 'RE',
                                                                       'PF', 'TF', 'YT', 'NC',
                                                                       'BL', 'MF', 'PM', 'WF'),
                                                         'GB' => array('IM', 'GG', 'JE'));
    /*
     * Checks if an creditor identifier (ci) is valid. Note that also if the ci is valid it does
     * not have to exist
     *
     * @param string $ci
     * @return string|false The valid iban or false if it is not valid
     */
    public static function checkCreditorIdentifier($ci)
    {
        $ci = preg_replace('/\s+/u', '', $ci);   // remove whitespaces
        $ci = strtoupper($ci);                   // todo does this break the ci?

        if(!self::checkRestrictedPersonIdentifierSEPA($ci))
            return false;

        $ciCopy = $ci;

        // remove creditor business code
        $nationalIdentifier = substr($ci, 7);
        $check = substr($ci, 0,4);
        $concat = $nationalIdentifier . $check;

        $concat = preg_replace('#[^a-zA-Z0-9]#u','',$concat);      // remove all non-alpha-numeric characters

        $concat = $check = str_replace(self::$alphabet, self::$alphabetValues, $concat);

        if(self::iso7064Mod97m10ChecksumCheck($concat))
            return $ciCopy;
        else
            return false;
    }

    /**
     * Checks if an iban is valid. Note that also if the iban is valid it does not have to exist
     *
*@param string $iban
     * @param array  $options valid keys:
     *                        - checkByCheckSum (boolean): If true, the IBAN checksum is
     *                        calculated (default:true)
     *                        - checkByFormat (boolean): If true, the format is checked by
     *                        regular expression (default: true)
     * @return string|false The valid iban or false if it is not valid
     */
    public static function checkIBAN($iban, $options = null)
    {
        $iban = preg_replace('/\s+/u', '' , $iban );     // remove whitespaces
        $iban = strtoupper($iban);

        if(!preg_match('/^' . self::PATTERN_IBAN . '$/',$iban))
            return false;

        $ibanCopy = $iban;

        if(!isset($options['checkByFormat']) || $options['checkByFormat'])
        {
            $countryCode = substr($iban,0,2);
            if(isset(self::$ibanPatterns[$countryCode])
                && !preg_match('/^' . self::$ibanPatterns[$countryCode] . '$/',$iban))
                return false;
        }

        if(!isset($options['checkByCheckSum']) || $options['checkByCheckSum'])
        {
            $iban = $check = str_replace(self::$alphabet, self::$alphabetValues, $iban);

            $bban  = substr($iban, 6);
            $check = substr($iban, 0, 6);

            $concat = $bban . $check;

            if( !self::iso7064Mod97m10ChecksumCheck($concat) )
                return false;
        }

        return $ibanCopy;
    }

    private static function iso7064Mod97m10ChecksumCheck($input)
    {
        $checksum = 0;
        $len = strlen($input);
        for($i = 1; $i  <= $len; $i++)
        {
            $checksum = (($checksum + self::$mod97Values[$i-1]*$input[$len-$i]) % 97);
        }

        return ($checksum == 1);
    }

    /**
     * Checks if a bic is valid. Note that also if the bic is valid it does not have to exist
     *
     * @param string $bic
     * @param array  $options Takes the following keys:
     *                        - `allowEmptyBic`: (bool) The BIC can be empty.
     *                        - `forceLongBic`: (bool) If the BIC has exact 8 characters, `forceLongBicStr`
     *                        is added. (default false)
     *                        - `forceLongBicStr`: string (default 'XXX')
     * @return string|false the valid bic or false if it is not valid
     */
    public static function checkBIC($bic, array $options = null)
    {
        $bic = preg_replace('/\s+/u', '' , $bic );   // remove whitespaces

        if(!empty($options['forceLongBic']) && strlen($bic) === 8)
            $bic .= empty($options['forceLongBicStr']) ? 'XXX' : $options['forceLongBicStr'];

        if(empty($bic) && !empty($options['allowEmptyBic']))
            return '';

        $bic = strtoupper($bic);                    // use only capital letters

        if(preg_match('/^' . self::PATTERN_BIC . '$/', $bic))
            return $bic;
        else
            return false;
    }

    /**
     * Checks if both IBANs do belong to the same country.
     * This function does not check if the IBANs are valid.
     *
     * @param string $iban1
     * @param string $iban2
     * @return bool
     */
    public static function isNationalTransaction($iban1, $iban2)
    {
        // remove whitespaces
        $iban1 = preg_replace('#\s+#','',$iban1);
        $iban2 = preg_replace('#\s+#','',$iban2);

        // check the country code
        if(stripos($iban1,substr($iban2,0,2)) === 0)
            return true;
        else
            return false;
    }

    /**
     * Checks if IBAN and BIC belong to the same country. If not, they also can not belong to
     * each other.
     *
     * @param string $iban
     * @param string $bic
     * @return bool
     */
    public static function crossCheckIbanBic($iban, $bic)
    {
        // remove whitespaces
        $iban = preg_replace('#\s+#','',$iban);
        $bic  = preg_replace('#\s+#','',$bic);

        // check the country code
        $ibanCountryCode = strtoupper(substr($iban,0,2));
        $bicCountryCode = strtoupper(substr($bic,4,2));

        if($ibanCountryCode === $bicCountryCode
            || (isset(self::$bicIbanCountryCodeExceptions[$ibanCountryCode])
                && in_array($bicCountryCode,self::$bicIbanCountryCodeExceptions[$ibanCountryCode])))
            return true;
        else
            return false;
    }

    private static function checkDateFormat($input)
    {
        $dateObj = \DateTime::createFromFormat('Y-m-d', $input);
        if($dateObj !== false && $input === $dateObj->format('Y-m-d'))
            return $input;
        else
            return false;
    }

    /**
     * Tries to convert the given date into the format YYYY-MM-DD (Y-m-d). Therefor it tries the
     * following input formats in the order of appearance: d.m.Y, d.m.y, j.n.Y, j.n.y, m.d.Y,
     * m.d.y, n.j.Y, n.j.y, Y/m/d, y/m/d, Y/n/j, y/n/j, Y.m.d, y.m.d, Y.n.j, y.n.j.
     * Notice that this method tries to interpret the first number as day-of-month. This can
     * lead to wrong dates if you have something like the 1st of April 2016 written as 04.01.2016.
     * This will be interpreted as the 4th of January 2016. This is why you have to call this
     * method on your owen risk and it is not included in the sanitize() method.
     *
     * @param string $input The date that should be reformatted
     * @param array  $preferredFormats An array of formats that will be checked first.
     * @return string|false The sanitized date or false, if it is not sanitizable.
     */
    public static function sanitizeDateFormat($input, array $preferredFormats = array())
    {
        $dateFormats = array('d.m.Y', 'd.m.y', 'j.n.Y', 'j.n.y', 'm.d.Y', 'm.d.y', 'n.j.Y', 'n.j.y',
                             'Y/m/d', 'y/m/d', 'Y/n/j', 'y/n/j', 'Y.m.d', 'y.m.d', 'Y.n.j', 'y.n.j');

        // input is already in the correct format?
        $dateObj = \DateTime::createFromFormat('Y-m-d',$input);
        if($dateObj !== false)
            return $input;

        foreach($preferredFormats as $format)
        {
            $dateObj = \DateTime::createFromFormat($format,$input);
            if($dateObj !== false)
                return $dateObj->format('Y-m-d');
        }

        foreach($dateFormats as $format)
        {
            $dateObj = \DateTime::createFromFormat($format,$input);
            if($dateObj !== false)
                return $dateObj->format('Y-m-d');
        }

        return false;
    }

    /**
     * Checks if the input has the format 'Y-m-d\TH:i:s'
     * @param string $input
     * @return string|false Returns $input if it is valid and false else.
     */
    public static function checkCreateDateTime($input)
    {
        $dateObj = \DateTime::createFromFormat('Y-m-d\TH:i:s', $input);
        if($dateObj !== false && $input === $dateObj->format('Y-m-d\TH:i:s'))
            return $input;
        else
            return false;
    }

    /**
     * Reformat a date string from a given format to the ISODate format. Notice: 20.13.2014 is
     * valid and becomes 2015-01-20.
     *
     * @param string $date A date string of the given input format
     * @param string $inputFormat default is the german format DD.MM.YYYY
     * @return string|false date as YYYY-MM-DD or false, if the input is not a date.
     */
    public static function getDate($date = null, $inputFormat = 'd.m.Y')
    {
        if(empty($date))
            $dateTimeObj = new \DateTime();
        else
            $dateTimeObj = \DateTime::createFromFormat($inputFormat, $date);

        if($dateTimeObj === false)
            return false;

        return $dateTimeObj->format('Y-m-d');
    }

    /**
     * Computes the next TARGET2 day (including today) with respect to a TARGET2 offset.
     *
     * @param int    $workdayOffset a positive number of workdays to skip.
     * @param string $today         if set, this date is used as today
     * @param string $inputFormat
     * @return string|false YYYY-MM-DD
     */
    public static function getDateWithOffset($workdayOffset, $today = null, $inputFormat = 'd.m.Y')
    {
        if(empty($today))
            $dateTimeObj = new \DateTime();
        else
            $dateTimeObj = \DateTime::createFromFormat($inputFormat, $today);

        if($dateTimeObj === false)
            return false;

        $isTargetDay = self::dateIsTargetDay($dateTimeObj);

        while( !$isTargetDay || $workdayOffset > 0 )
        {
            $dateTimeObj->modify('+1 day');

            if($isTargetDay)
                $workdayOffset--;

            $isTargetDay = self::dateIsTargetDay($dateTimeObj);
        }

        return $dateTimeObj->format('Y-m-d');
    }

    /**
     * Returns the target date, if it has at least the given offset of TARGET2 days form today. Else
     * the earliest date that respects the offset is returned.
     *
     * @param string $target
     * @param int    $workdayMinOffset
     * @param string $inputFormat
     * @param string $today
     * @return string
     */
    public static function getDateWithMinOffsetFromToday($target, $workdayMinOffset, $inputFormat = 'd.m.Y', $today = null)
    {
        $targetDateObj = \DateTime::createFromFormat($inputFormat,$target);

        $earliestDate = self::getDateWithOffset($workdayMinOffset, $today, $inputFormat);

        if($targetDateObj === false || $earliestDate === false)
            return false;

        $earliestDateObj = new \DateTime($earliestDate);

        $isTargetDay = self::dateIsTargetDay($targetDateObj);
        while( !$isTargetDay )
        {
            $targetDateObj->modify('+1 day');
            $isTargetDay = self::dateIsTargetDay($targetDateObj);
        }

        $diff = $targetDateObj->diff($earliestDateObj);
        if($diff->invert === 1)      // target > earliest
            return $targetDateObj->format('Y-m-d');
        else
            return $earliestDateObj->format('Y-m-d');
    }

    /**
     * Checks if $date is a SEPA TARGET day. Every day is a TARGET day except for saturdays, sundays
     * new year's day, good friday, easter monday, the may holiday, first and second christmas holiday.
     * @param \DateTime $date
     * @return bool
     */
    private static function dateIsTargetDay(\DateTime $date)
    {
        // $date is a saturday or sunday?
        if($date->format('N') === '6' || $date->format('N') === '7')
            return false;

        $day = $date->format('m-d');
        if($day === '01-01'             // new year's day
            || $day === '05-01'         // labour day
            || $day === '12-25'         // first christmas day
            || $day === '12-26')        // second christmas day
            return false;

        $year = $date->format('Y');
        $daysToEasterSunday = easter_days((int) $year);
        $goodFriday = \DateTime::createFromFormat('Y-m-d', $year . '-03-21')
            ->modify('+' . ($daysToEasterSunday - 2) . ' days')
            ->format('m-d');
        $easterMonday = \DateTime::createFromFormat('Y-m-d', $year . '-03-21')
            ->modify('+' . ($daysToEasterSunday + 1) . ' days')
            ->format('m-d');

        if($day === $goodFriday || $day === $easterMonday)
            return false;

        return true;
    }

    /**
     * @param mixed[]             $input Reference to an array
     * @param int|string|string[] $keys  The keys of the multidimensional array in order of
     *                                   appearance. e.g. `['key1','key2']` checks
     *                                   `$arr['key1']['key2']`
     * @return mixed|false Returns the value of the field or null if the field does not exist.
     */
    private static function getValFromMultiDimInput(array &$input, $keys)
    {
        $key = is_array($keys) ? array_shift($keys) : $keys;
        if( !isset( $input[$key] ) )
            return false;

        if( is_array($keys) && !empty( $keys ) ) // another dimension
            return self::getValFromMultiDimInput($input[$key], $keys);
        else
            return $input[$key];
    }

    /**
     * Checks if the input holds for the field.
     *
     * @param string $field   Valid fields are: 'orgnlcdtrschmeid_id','ci','msgid','pmtid','pmtinfid',
     *                        'orgnlmndtid','mndtid','initgpty','cdtr','dbtr','orgnlcdtrschmeid_nm',
     *                        'ultmtcdrt','ultmtdebtr','rmtinf','orgnldbtracct_iban','iban','bic',
     *                        'ccy','amendment', 'btchbookg','instdamt','seqtp','lclinstrm',
     *                        'elctrncsgntr','reqdexctndt','purp','ctgypurp','orgnldbtragt'
     * @param mixed  $input
     * @param array  $options See `checkBIC()`, `checkIBAN()` and `checkLocalInstrument()` for details.
     * @param int    $version Can be used to specify one of the `SEPA_PAIN_*` constants.
     * @return false|mixed The checked input or false, if it is not valid
     */
    public static function check($field, $input, array $options = null, $version = null)
    {
        $field = strtolower($field);
        switch($field)      // fall-through's are on purpose
        {
            case 'orgnlcdtrschmeid_id':
            case 'ci': return self::checkCreditorIdentifier($input);
            case 'msgid':
            case 'pmtid':   // next line
            case 'pmtinfid': return self::checkRestrictedIdentificationSEPA1($input);
            case 'orgnlmndtid':
            case 'mndtid': return $version === self::SEPA_PAIN_001_001_03_GBIC
                                    || $version === self::SEPA_PAIN_008_001_02_GBIC
                            ? self::checkRestrictedIdentificationSEPA1($input)
                            : self::checkRestrictedIdentificationSEPA2($input);
            case 'initgpty':                                // cannot be empty (and the following things also)
            case 'cdtr':                                    // cannot be empty (and the following things also)
            case 'dbtr': if(empty($input)) return false;    // cannot be empty
            case 'orgnlcdtrschmeid_nm':
            case 'ultmtcdtr':
            case 'ultmtdbtr': return (self::checkLength($input, 70) && self::checkCharset($input)) ? $input : false;
            case 'rmtinf': return (self::checkLength($input, 140) && self::checkCharset($input)) ? $input : false;
            case 'orgnldbtracct_iban':
            case 'iban': return self::checkIBAN($input,$options);
            case 'bic': return self::checkBIC($input,$options);
            case 'ccy': return self::checkActiveOrHistoricCurrencyCode($input);
            case 'amdmntind':
            case 'btchbookg': return self::checkBoolean($input);
            case 'instdamt': return self::checkAmountFormat($input);
            case 'seqtp': return self::checkSeqType($input);
            case 'lclinstrm': return self::checkLocalInstrument($input, $options);
            case 'elctrncsgntr': return (self::checkLength($input, 1025) && self::checkCharset($input)) ? $input : false;
            case 'dtofsgntr':
            case 'reqdcolltndt':
            case 'reqdexctndt': return self::checkDateFormat($input);
            case 'purp': return self::checkPurpose($input);
            case 'ctgypurp': return self::checkCategoryPurpose($input);
            case 'orgnldbtragt': return $input;     // nothing to check here
            default: return false;
        }
    }

    /**
     * This function checks if the index of the inputArray exists and if the input is valid. The
     * function can be called as `checkInput($fieldName,$_POST,['input',$fieldName],$options)`
     * and equals `check($fieldName,$_POST['input'][$fieldName],$options)`, but checks first, if
     * the index exists.
     * @param string $field     see `check()` for valid values.
     * @param array $inputArray
     * @param string|int|mixed[] $inputKeys
     * @param array $options    see `check()` for valid values.
     * @return mixed|false
     */
    public static function checkInput($field, array &$inputArray, $inputKeys, array $options = null)
    {
        $value = self::getValFromMultiDimInput($inputArray,$inputKeys);

        if($value === false)
            return false;
        else
            return self::check($field,$value,$options);
    }

    /**
     * This function checks if the index of the inputArray exists and if the input is valid. The
     * function can be called as `sanitizeInput($fieldName,$_POST,['input',$fieldName],$flags)`
     * and equals `sanitize($fieldName,$_POST['input'][$fieldName],$flags)`, but checks first, if
     * the index exists.
     * @param string $field     see `sanitize()` for valid values.
     * @param array $inputArray
     * @param string|int|mixed[] $inputKeys
     * @param int   $flags    see `sanitize()` for valid values.
     * @return mixed|false
     */
    public static function sanitizeInput($field, array &$inputArray, $inputKeys, $flags = 0)
    {
        $value = self::getValFromMultiDimInput($inputArray,$inputKeys);

        if($value === false)
            return false;
        else
            return self::sanitize($field,$value,$flags);
    }

    /**
     * Checks the input and if it is not valid it tries to sanitize it.
     *
     * @param string $field all fields check and/or sanitize supports
     * @param mixed  $input
     * @param int    $flags   see `sanitize()` for details
     * @param array  $options see `check()` for details
     * @return mixed|false
     */
    public static function checkAndSanitize($field, $input, $flags = 0, array $options = null)
    {
        $checkedInput = self::check($field, $input, $options);
        if($checkedInput !== false)
            return $checkedInput;

        return self::sanitize($field,$input,$flags);
    }

    /**
     * This function checks if the index of the inputArray exists and if the input is valid. The
     * function can be called as `checkAndSanitizeInput($fieldName,$_POST,['input',$fieldName],$flags,$options)`
     * and equals `checkAndSanitize($fieldName,$_POST['input'][$fieldName],$flags,$options)`, but checks first, if
     * the index exists.
     *
     * @param string             $field   see `checkAndSanitize()` for valid values.
     * @param array              $inputArray
     * @param string|int|mixed[] $inputKeys
     * @param int                $flags   see `checkAndSanitize()` for valid values.
     * @param array              $options see `checkAndSanitize()` for valid values.
     * @return false|mixed
     */
    public static function checkAndSanitizeInput($field, array &$inputArray, $inputKeys, $flags = 0, array $options = null)
    {
        $value = self::getValFromMultiDimInput($inputArray,$inputKeys);

        if($value === false)
            return false;
        else
            return self::checkAndSanitize($field,$value,$flags,$options);
    }

    /**
     * @param array $inputs A reference to an input array (field => value)
     * @param int   $flags  Flags for sanitizing
     * @param array $options Options for checking
     * @return true|string returns true, if everything is ok or could be sanitized. Otherwise a
     *                     string with fields, that could not be sanitized is returned.
     */
    public static function checkAndSanitizeAll(array &$inputs, $flags = 0, array $options = null)
    {
        $fieldsWithErrors = array();
        foreach($inputs as $field => &$input)
        {
            $input = self::checkAndSanitize($field, $input, $flags, $options);
            if($input === false)
                $fieldsWithErrors[] = $field;
        }

        if(empty($fieldsWithErrors))
            return true;
        else
            return implode(', ', $fieldsWithErrors);
    }

    public static function sanitizeShortText($input,$allowEmpty = false, $flags = 0)
    {
        $res = self::sanitizeLength(self::replaceSpecialChars($input, $flags), 70);

        if($allowEmpty || !empty($res))
            return $res;

        return false;
    }

    public static function sanitizeLongText($input,$allowEmpty = false, $flags = 0)
    {
        $res = self::sanitizeLength(self::replaceSpecialChars($input, $flags), 140);

        if($allowEmpty || !empty($res))
            return $res;

        return false;
    }

    /**
     * Tries to sanitize the the input so it fits in the field.
     *
     * @param string $field Valid fields are: 'ultmtcdrt', 'ultmtdebtr',
     *                      'orgnlcdtrschmeid_nm', 'initgpty', 'cdtr', 'dbtr', 'rmtinf'
     * @param mixed  $input
     * @param int    $flags Flags used in replaceSpecialChars()
     * @return mixed|false The sanitized input or false if the input is not sanitizeable or
     *                      invalid also after sanitizing.
     */
    public static function sanitize($field, $input, $flags = 0)
    {
        $field = strtolower($field);
        switch($field)          // fall-through's are on purpose
        {
            case 'ultmtcdrt':
            case 'ultmtdebtr': return self::sanitizeShortText($input,true,$flags);
            case 'orgnlcdtrschmeid_nm':
            case 'initgpty':
            case 'cdtr':
            case 'dbtr':
                return self::sanitizeShortText($input,false,$flags);
            case 'rmtinf': return self::sanitizeLongText($input,true,$flags);
            default: return false;
        }
    }

    public static function checkRequiredCollectionKeys(array $inputs, $version)
    {
        switch($version)    // fall-through's are on purpose
        {
            case self::SEPA_PAIN_001_002_03:
                $requiredKeys = array('pmtInfId', 'dbtr', 'iban', 'bic');
                break;
            case self::SEPA_PAIN_001_001_03:
            case self::SEPA_PAIN_001_001_03_GBIC:
            case self::SEPA_PAIN_001_003_03:
                $requiredKeys = array('pmtInfId', 'dbtr', 'iban');
                break;
            case self::SEPA_PAIN_008_002_02:
                $requiredKeys = array('pmtInfId', 'lclInstrm', 'seqTp', 'cdtr', 'iban', 'bic', 'ci');
                break;
            case self::SEPA_PAIN_008_001_02:
            case self::SEPA_PAIN_008_001_02_GBIC:
            case self::SEPA_PAIN_008_003_02:
                $requiredKeys = array('pmtInfId', 'lclInstrm', 'seqTp', 'cdtr', 'iban', 'ci');
                break;
            default:
                return false;
        }

        return self::containsAllKeys($inputs,$requiredKeys);
    }

    public static function checkRequiredPaymentKeys(array $inputs, $version)
    {
        switch($version)
        {
            case self::SEPA_PAIN_001_002_03:
                $requiredKeys = array('pmtId', 'instdAmt', 'iban', 'bic', 'cdtr');
                break;
            case self::SEPA_PAIN_001_001_03:
            case self::SEPA_PAIN_001_001_03_GBIC:
            case self::SEPA_PAIN_001_003_03:
                $requiredKeys = array('pmtId', 'instdAmt', 'iban', 'cdtr');
                break;
            case self::SEPA_PAIN_008_002_02:
                $requiredKeys = array('pmtId', 'instdAmt', 'mndtId', 'dtOfSgntr', 'dbtr', 'iban','bic');
                break;
            case self::SEPA_PAIN_008_001_02:
            case self::SEPA_PAIN_008_001_02_GBIC:
            case self::SEPA_PAIN_008_003_02:
                $requiredKeys = array('pmtId', 'instdAmt', 'mndtId', 'dtOfSgntr', 'dbtr', 'iban');
                break;
            default: return false;
        }

        return self::containsAllKeys($inputs,$requiredKeys);
    }

    /**
     * Checks if $arr misses one of the given $keys
     * @param array $arr
     * @param array $keys
     * @return bool false, if at least one key is missing, else true
     */
    public static function containsAllKeys(array $arr, array $keys)
    {
        foreach($keys as $key)
        {
            if( !isset( $arr[$key] ) )
                return false;
        }

        return true;
    }

    /**
     * Checks if $arr not contains any key of $keys
     * @param array $arr
     * @param array $keys
     * @return bool true, if $arr contains not even on the the keys, else false
     */
    public static function containsNotAnyKey(array $arr, array $keys)
    {
        foreach ($keys as $key) {
            if (isset($arr[$key]))
                return false;
        }

        return true;
    }

    /**
     * Checks if the currency code has a valid format. Also if it has a valid format it has not to exist.
     * If it has a valid format it will also be changed to upper case only.
     * @param string $ccy
     * @return string|false The valid input (in upper case only) or false if it is not valid.
     */
    private static function checkActiveOrHistoricCurrencyCode( $ccy )
    {
        $ccy = strtoupper($ccy);

        if(preg_match('/^[A-Z]{3}$/', $ccy))
            return $ccy;
        else
            return false;
    }

    /**
     * Checks if $bbi is a valid batch booking indicator. Returns 'true' for "1", "true", "on"
     * and "yes", returns 'false' for "0", "false", "off", "no", and ""
     *
     * @param mixed $input
     * @return string|false The batch booking indicator (in lower case only) or false if not
     *                      valid
     */
    private static function checkBoolean($input )
    {
        $bbi = filter_var($input,FILTER_VALIDATE_BOOLEAN,FILTER_NULL_ON_FAILURE);

        if($bbi === true)
            return 'true';

        if($bbi === false)
            return 'false';

        return false;
    }

    /**
     * @param string $input
     * @return string|bool
     */
    private static function checkRestrictedIdentificationSEPA1($input)
    {
        if(preg_match('#^' . self::PATTERN_RESTRICTED_IDENTIFICATION_SEPA1 . '$#', $input))
            return $input;
        else
            return false;
    }

    /**
     * @param string $input
     * @return string|bool
     */
    private static function checkRestrictedIdentificationSEPA2($input)
    {
        if(preg_match('#^' . self::PATTERN_RESTRICTED_IDENTIFICATION_SEPA2 . '$#', $input))
            return $input;
        else
            return false;
    }

    /**
     * @param string $input
     * @return string|bool
     */
    private static function checkRestrictedPersonIdentifierSEPA($input)
    {
        if(preg_match('#^' . self::PATTERN_CREDITOR_IDENTIFIER . '$#',$input))
            return $input;
        else
            return false;
    }

    /**
     * Checks if the length of the input string not longer than the entered length
     *
     * @param string $input
     * @param int $maxLen
     * @return bool
     */
    private static function checkLength( $input, $maxLen )
    {
        return !isset($input[$maxLen]);     // takes the string as char array
    }

    /**
     * Shortens the input string to the max length if it is to long.
     * @param string $input
     * @param int $maxLen
     * @return string sanitized string
     */
    public static function sanitizeLength($input, $maxLen)
    {
        if(isset($input[$maxLen]))     // take string as array of chars
            return substr($input,0,$maxLen);
        else
            return $input;
    }

    /**
     * Replaces all special chars like á, ä, â, à, å, ã, æ, Ç, Ø, Š, ", ’ and & by a latin character.
     * All special characters that cannot be replaced by a latin char (such like quotes) will
     * be removed as long as they cannot be converted. See http://www.europeanpaymentscouncil.eu/index.cfm/knowledge-bank/epc-documents/sepa-requirements-for-an-extended-character-set-unicode-subset-best-practices/
     * for more information about converting characters.
     *
     * @param string $str
     * @param int    $flags Use the SepaUtilities::FLAG_ALT_REPLACEMENT_* and SepaUtilities::FLAG_NO_REPLACEMENT_*
     *                      constants. FLAG_ALT_REPLACEMENT_* will ignore the best practice replacement
     *                      and use a more common one. You can use more than one flag by using
     *                      the | (bitwise or) operator. FLAG_NO_REPLACEMENT_* tells the function
     *                      not to replace the character group.
     * @return string
     */
    public static function replaceSpecialChars($str, $flags = 0)
    {
        if($flags === 0)
        {
            $specialCharsReplacement =& self::$specialCharsReplacement; // reference
            $charExceptions = '';
        }
        else
        {
            $specialCharsReplacement = self::$specialCharsReplacement;  // copy
            $charExceptions = '';

            if( $flags & self::FLAG_ALT_REPLACEMENT_GERMAN)
                self::changeArrayValuesByAssocArray($specialCharsReplacement,array('Ä' => 'Ae', 'ä' => 'ae', 'Ö' => 'Oe', 'ö' => 'oe', 'Ü' => 'Ue', 'ü' => 'ue', 'ß' => 'ss'));
            if($flags & self::FLAG_NO_REPLACEMENT_GERMAN)
            {
                self::changeArrayValuesToKeys($specialCharsReplacement, array('Ä', 'ä', 'Ö', 'ö', 'Ü', 'ü', 'ß'));
                $charExceptions .= 'ÄäÖöÜüß';
            }
        }

        // remove characters
        $str = str_replace(array('"','&','<','>'),'',$str);

        // replace all kinds of whitespaces by a space
        $str = preg_replace('#\s+#u',' ',$str);

        // special replacement for some characters (incl. greek and cyrillic)
        $str = strtr($str,$specialCharsReplacement);

        // replace everything not allowed in sepa files by . (a dot)
        $str = preg_replace('#[^a-zA-Z0-9/\-?:().,\'+ ' . $charExceptions . ']#u','.',$str);

        // remove leading and closing whitespaces
        return trim($str);
    }

    private static function checkCharset($str)
    {
        return (boolean) preg_match('#^[a-zA-Z0-9/\-?:().,\'+ ]*$#', $str);
    }

    /**
     * Checks if the amount fits the format: A float with only two decimals, not lower than 0.01,
     * not greater than 999,999,999.99.
     *
     * @param mixed $amount float or string with or without thousand separator (use , or .). You
     *                      can use '.' or ',' as decimal point, but not one sign as thousand separator
     *                      and decimal point. So 1234.56; 1,234.56; 1.234,56; 1234,56 ar valid
     *                      inputs.
     * @return float|false
     */
    private static function checkAmountFormat( $amount )
    {
        // $amount is a string -> check for '1,234.56'
        $result = filter_var($amount, FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND);

        if($result === false)
            $result = filter_var(strtr($amount,array(',' => '.', '.' => ',')), FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND);

        if($result === false || $result < 0.01 || $result > 999999999.99 || round($result,2) != $result)
            return false;

        return $result;
    }

    /**
     * Checks if the sequence type is valid.
     *
     * @param string $seqTp
     * @return string|false
     */
    private static function checkSeqType($seqTp)
    {
        $seqTp = strtoupper($seqTp);

        if( in_array($seqTp, array(self::SEQUENCE_TYPE_FIRST, self::SEQUENCE_TYPE_RECURRING,
                                   self::SEQUENCE_TYPE_ONCE, self::SEQUENCE_TYPE_FINAL)) )
            return $seqTp;

        return false;
    }

    /**
     * @param string $input
     * @param array $options Can contain the key `version` with values `SepaUtilities::SEPA_PAIN_008_*`
     * @return bool|string
     */
    private static function checkLocalInstrument($input, array $options = null)
    {
        $version = empty($options['version']) ? self::SEPA_PAIN_008_002_02 : $options['version'];

        $input = strtoupper($input);

        switch($version)    // fall-through's are on purpose
        {
            case self::SEPA_PAIN_008_001_02:
            case self::SEPA_PAIN_008_001_02_GBIC:
            case self::SEPA_PAIN_008_002_02:
                $validCases = array(self::LOCAL_INSTRUMENT_CORE_DIRECT_DEBIT,
                                    self::LOCAL_INSTRUMENT_BUSINESS_2_BUSINESS);
                break;
            case self::SEPA_PAIN_008_003_02:
                $validCases = array(self::LOCAL_INSTRUMENT_CORE_DIRECT_DEBIT,
                                    self::LOCAL_INSTRUMENT_CORE_DIRECT_DEBIT_D_1,
                                    self::LOCAL_INSTRUMENT_BUSINESS_2_BUSINESS);
                break;
            default:
                return false;
        }

        if( in_array($input, $validCases) )
            return $input;

        return false;
    }

    private static function checkCategoryPurpose($input)
    {
        $validValues = array('BONU', 'CASH', 'CBLK', 'CCRD', 'CORT', 'DCRD', 'DIVI', 'EPAY',
                             'FCOL', 'GOVT', 'HEDG', 'ICCP', 'IDCP', 'INTC', 'INTE', 'LOAN',
                             'OTHR', 'PENS', 'SALA', 'SECU', 'SSBE', 'SUPP', 'TAXS', 'TRAD',
                             'TREA', 'VATX', 'WHLD');

        $input = strtoupper($input);

        if(in_array($input,$validValues))
            return $input;

        return false;
    }

    private static function checkPurpose($input)
    {
        $validValues = array('CBLK', 'CDCB', 'CDCD', 'CDCS', 'CDDP', 'CDOC', 'CDQC', 'ETUP',
                             'FCOL', 'MTUP', 'ACCT', 'CASH', 'COLL', 'CSDB', 'DEPT', 'INTC',
                             'LIMA', 'NETT', 'AGRT', 'AREN', 'BEXP', 'BOCE', 'COMC', 'CPYR',
                             'GDDS', 'GDSV', 'GSCB', 'LICF', 'POPE', 'ROYA', 'SCVE', 'SUBS',
                             'SUPP', 'TRAD', 'CHAR', 'COMT', 'CLPR', 'DBTC', 'GOVI', 'HLRP',
                             'INPC', 'INSU', 'INTE', 'LBRI', 'LIFI', 'LOAN', 'LOAR', 'PENO',
                             'PPTI', 'RINP', 'TRFD', 'ADMG', 'ADVA', 'BLDM', 'CBFF', 'CBFR',
                             'CCRD', 'CDBL', 'CFEE', 'CGDD', 'COST', 'CPKC', 'DCRD', 'EDUC',
                             'FAND', 'FCPM', 'GOVT', 'ICCP', 'IDCP', 'IHRP', 'INSM', 'IVPT',
                             'MSVC', 'NOWS', 'OFEE', 'OTHR', 'PADD', 'PTSP', 'RCKE', 'RCPT',
                             'REBT', 'REFU', 'RENT', 'RIMB', 'STDY', 'TBIL', 'TCSC', 'TELI',
                             'WEBI', 'ANNI', 'CAFI', 'CFDI', 'CMDT', 'DERI', 'DIVD', 'FREX',
                             'HEDG', 'INVS', 'PRME', 'SAVG', 'SECU', 'SEPI', 'TREA', 'ANTS',
                             'CVCF', 'DMEQ', 'DNTS', 'HLTC', 'HLTI', 'HSPC', 'ICRF', 'LTCF',
                             'MDCS', 'VIEW', 'ALLW', 'ALMY', 'BBSC', 'BECH', 'BENE', 'BONU',
                             'COMM', 'CSLP', 'GVEA', 'GVEB', 'GVEC', 'GVED', 'PAYR', 'PENS',
                             'PRCP', 'SALA', 'SSBE', 'AEMP', 'GFRP', 'GWLT', 'RHBS', 'ESTX',
                             'FWLV', 'GSTX', 'HSTX', 'INTX', 'NITX', 'PTXP', 'RDTX', 'TAXS',
                             'VATX', 'WHLD', 'TAXR', 'AIRB', 'BUSB', 'FERB', 'RLWY', 'TRPT',
                             'CBTV', 'ELEC', 'ENRG', 'GASB', 'NWCH', 'NWCM', 'OTLC', 'PHON',
                             'UBIL', 'WTER');

        $input = strtoupper($input);

        if( in_array($input, $validValues) )
            return $input;

        return false;
    }

    /**
     * Performs $array[$key] = $value for all $key => $value pairs in $newValues.
     * @param string[] $array
     * @param string[] $newValues An assoc array with keys that exists in $array
     */
    private static function changeArrayValuesByAssocArray(&$array, $newValues)
    {
        foreach($newValues as $key => $val)
            $array[$key] = $val;
    }

    /**
     * Performs $array[$key] = $key for all values in $keys.
     * @param $array
     * @param $keys
     */
    private static function changeArrayValuesToKeys(&$array, $keys)
    {
        foreach($keys as $key)
            $array[$key] = $key;
    }
}