Anlage 1. Programmcode zur Tarifberechnung

Die interaktiven Darstellungen in dieser Arbeit basieren direkt auf den Tarifformeln des § 32a Abs. 1 EStG. Diese Formeln wurden in einem JavaScript-Modul implementiert, das zusätzlich auch den Code zur Berechnung des relativen Verlusts aus der Kalten Progression sowie zur Tarifanpassung enthält. Die Steuerberechnung wurde mit dem BMF-Steuerrechner überprüft.

Die TaxScale-Klasse ist in dieser Seite eingebunden und kann in der Browser-Konsole in den Dev Tools bspw. mit new TaxScale(2019) ausprobiert werden.

/**
 * TaxScale
 * Calculates German ESt
 *
 * @author    Lukas Bestle <project-bachelor@lukasbestle.com>
 * @copyright 2019 Lukas Bestle
 * @license   MIT <https://opensource.org/licenses/MIT>
 *
 * @param {number|Array} data Tax year or custom data array
 */
export default function TaxScale(data) {
    if (typeof data !== 'object') {
        this.year = data;
        data = TaxScale.getDataForYear(data);
    } else {
        this.year = null;
    }

    // make a local copy of the data
    this.data = data.slice(0);
};

/**
 * Returns the available years with
 * their ID and label
 *
 * @return {Object}
 */
TaxScale.years = function() {
    var years    = {},
        lastYear = null;

    // build an object of `from: to` pairs...
    Object.keys(data).forEach(function(year) {
        if (lastYear !== null && year - 1 > lastYear) {
            // at least one year was skipped, update the last year
            years[lastYear] = year - 1;
        }

        years[year] = lastYear = year;
    });

    // ...and convert that to `id: label` pairs
    for (var yearFrom in years) {
        var yearTo = years[yearFrom];

        if (yearFrom === yearTo) {
            years[yearFrom] = yearFrom;
        } else {
            years[yearFrom] = yearFrom + '–' + yearTo;
        }
    }

    return years;
};

/**
 * Returns the data for a given tax year
 *
 * @param  {number} year Tax year
 * @return {Array}
 */
TaxScale.getDataForYear = function(year) {
    // check variable type
    if (typeof year !== 'number') {
        throw new Error('Year must be a number.');
    }

    // check if we are out of bounds
    var dataYears = Object.keys(data);
    var validityMin = dataYears[0];           // year that is defined first
    var validityMax = dataYears.slice(-1)[0]; // year that is defined last
    if (year < validityMin || year > validityMax) {
        throw new Error('The given year ' + year + ' is not implemented.');
    }

    // find a matching year from the data object;
    // if the year does not exist, go back in time
    // (data of older year is still valid in this case)
    var vars;
    while (!vars) {
        vars = data[year];
        year--;
    }

    return vars;
};

/**
 * Returns a scale variable by name
 *
 * @param  {string} variable Variable name
 * @return {number}          Variable value for the current year
 */
TaxScale.prototype.getVariable = function(variable) {
    var key = variables.indexOf(variable);
    if (key < 0) {
        throw new Error('Variable type ' + variable + ' does not exist.');
    }

    return this.data[key];
};

/**
 * Overwrites a specified scale variable
 *
 * @param {string} variable Variable name
 * @param {number} value    New value
 */
TaxScale.prototype.setVariable = function(variable, value) {
    var key = variables.indexOf(variable);
    if (key < 0) {
        throw new Error('Variable type ' + variable + ' does not exist.');
    }

    this.data[key] = value;

    // the scale is no longer the one of any year
    this.year = null;
};

/**
 * Raises the exemption of the current scale by a
 * specified amount
 *
 * @param  {number}   percentage Percentage to raise exemption by
 * @return {TaxScale}            Instance
 */
TaxScale.prototype.raiseExemption = function(percentage) {
    return this.raiseVariables('exemption', percentage);
};

/**
 * Stretches the current scale by a specified amount
 *
 * @param  {number}   percentage Percentage to stretch scale by
 * @return {TaxScale}            Instance
 */
TaxScale.prototype.stretch = function(percentage) {
    return this.raiseVariables([
        'exemption',
        'threshold1',
        'threshold2',
        'threshold3'
    ], percentage);
};

/**
 * Raises the specified threshold variable(s) by a specified amount and
 * recalculates the scale data;
 * can be called multiple times with different variables and percentages
 *
 * @param  {Array|string} variables  Scale variable or array of multiple variables
 * @param  {number}       percentage Percentage to raise variable(s) by
 * @return {TaxScale}                Instance
 */
TaxScale.prototype.raiseVariables = function(variables, percentage) {
    // support for strings as argument
    if (typeof variables === 'string') {
        variables = [variables];
    }

    // raise the specified variables
    for (var variable in variables) {
        variable = variables[variable];
        this.setVariable(variable, Math.round(this.getVariable(variable) * (1 + percentage)));
    }

    // recalculate data once after everything has been raised
    recalculateData(this);

    return this;
};

/**
 * Returns the ESt for a given zvE
 *
 * @param  {number} zvE "Zu versteuerndes Einkommen"
 * @return {Object}     Object with zvE, amount, amountExact, average and marginal
 */
TaxScale.prototype.calculate = function(zvE) {
    // § 32a Abs. 1 S. 3–5 EStG
    var x = Math.floor(zvE);
    var y = (x - this.getVariable('exemption'))  / 10000;
    var z = (x - this.getVariable('threshold1')) / 10000;

    // § 32a Abs. 1 S. 2 EStG
    // marginal rates based on https://www.fbmn.h-da.de/~pfeifer/Einkommensteuertarif.pdf;
    // marginal rate for threshold zvEs is the one on the left of the threshold (like in the BMF Steuerrechner)
    var amount, marginal;
    if (x <= this.getVariable('exemption')) {
        amount   = 0;
        marginal = 0;
    } else if (x <= this.getVariable('threshold1')) {
        amount   = (this.getVariable('factor1') * y + this.getVariable('rate1')) * y;
        marginal = (2 * this.getVariable('factor1') * y + this.getVariable('rate1')) / 10000;
    } else if (x <= this.getVariable('threshold2')) {
        amount   = (this.getVariable('factor2') * z + this.getVariable('rate2')) * z + this.getVariable('constant2');
        marginal = (2 * this.getVariable('factor2') * z + this.getVariable('rate2')) / 10000;
    } else if (x <= this.getVariable('threshold3')) {
        amount   = this.getVariable('rate3') * x - this.getVariable('constant3');
        marginal = this.getVariable('rate3');
    } else {
        amount   = this.getVariable('rate4') * x - this.getVariable('constant4');
        marginal = this.getVariable('rate4');
    }

    // § 32a Abs. 1 S. 6 EStG
    var amountRounded = Math.floor(amount);

    return {
        zvE:         x,
        amount:      amountRounded,
        amountExact: amount,
        average:     (x > 0)? amountRounded / x : 0,
        marginal:    marginal
    };
};

/**
 * Calculates the loss from bracket creep for a given zvE
 * relative to the adjusted zvE
 *
 * @param  {number}   zvEOld             "Zu versteuerndes Einkommen"
 * @param  {number}   inflation          Inflation rate
 * @param  {TaxScale} comparisonTaxScale Base scale to compare against (current one if undefined)
 * @return {number}                      Relative loss percentage
 */
TaxScale.prototype.calculateRelativeLoss = function(zvEOld, inflation, comparisonTaxScale) {
    if (comparisonTaxScale === undefined) {
        comparisonTaxScale = this;
    }

    // adjust zvE to inflation
    var zvENew = zvEOld * (1 + inflation);

    // calculate actual and inflation-adjusted tax amounts
    var taxOld  = comparisonTaxScale.calculate(zvEOld).amount;
    var taxNewI = taxOld * (1 + inflation);
    var taxNewT = this.calculate(zvENew).amount;

    // calculate absolute loss
    var lossAbsolute = taxNewT - taxNewI;

    // calculate relative loss;
    // avoid NaN for zvENew === 0
    return (lossAbsolute / zvENew) || 0;
};

/**
 * Recalculates the scale data based on new thresholds or marginal rates
 * Can be called via:
 *  taxScale.raiseExemption()
 *  taxScale.stretch()
 *  taxScale.raiseVariables()
 *
 * @param {TaxScale} taxScale TaxScale instance to recalculate
 */
function recalculateData(taxScale) {
    // progressive zone 1
    var factor1 = 5000 * (taxScale.getVariable('rate2') - taxScale.getVariable('rate1')) /
                         (taxScale.getVariable('threshold1') - taxScale.getVariable('exemption'));
    factor1 = Math.round(factor1 * 100) / 100;
    taxScale.setVariable('factor1', factor1);

    // progressive zone 2
    var y = (taxScale.getVariable('threshold1') - taxScale.getVariable('exemption')) / 10000;
    var constant2 = (taxScale.getVariable('factor1') * y + taxScale.getVariable('rate1')) * y;
    constant2 = Math.round(constant2 * 100) / 100;
    taxScale.setVariable('constant2', constant2);
    var factor2 = 5000 * (taxScale.getVariable('rate3') * 10000 - taxScale.getVariable('rate2')) /
                         (taxScale.getVariable('threshold2') - taxScale.getVariable('threshold1'));
    factor2 = Math.round(factor2 * 100) / 100;
    taxScale.setVariable('factor2', factor2);

    // proportional zone 1
    var taxBelow3 = taxScale.calculate(taxScale.getVariable('threshold2')).amountExact;
    var constant3 = taxScale.getVariable('rate3') * taxScale.getVariable('threshold2') - taxBelow3;
    constant3 = Math.round(constant3 * 100) / 100;
    taxScale.setVariable('constant3', constant3);

    // proportional zone 2
    var taxBelow4 = taxScale.calculate(taxScale.getVariable('threshold3')).amountExact;
    var constant4 = taxScale.getVariable('rate4') * taxScale.getVariable('threshold3') - taxBelow4;
    constant4 = Math.round(constant4 * 100) / 100;
    taxScale.setVariable('constant4', constant4);
}

// names for the data variables and
// their order in the data array
var variables = [
    'exemption',
    'threshold1',
    'factor1',
    'rate1',
    'threshold2',
    'factor2',
    'rate2',
    'constant2',
    'threshold3',
    'rate3',
    'constant3',
    'rate4',
    'constant4',
];

/**
 * Implementation data for each year from § 32a Abs. 1 S. 2 EStG:
 *
 * ----
 *
 * Sie beträgt im Veranlagungszeitraum xxxx vorbehaltlich [...]
 * jeweils in Euro für zu versteuernde Einkommen
 *
 * 1. bis {exemption} Euro (Grundfreibetrag):
 *    0;
 * 2. von {exemption + 1} Euro bis {threshold1} Euro:
 *    ({factor1} · y + {rate1}) · y;
 * 3. von {threshold1 + 1} Euro bis {threshold2} Euro:
 *    ({factor2} · z + {rate2}) · z + {constant2};
 * 4. von {threshold2 + 1} Euro bis {threshold3} Euro:
 *    {rate3} · x – {constant3};
 * 5. von {threshold3 + 1} Euro an:
 *    {rate4} · x – {constant4}.
 *
 * ---
 *
 * If a year is not defined, the data from the year before
 * is still valid and therefore not duplicated!
 */
var data = {
    2007: [
        /*exemption*/    7664.00,
        /*threshold1*/  12739.00,
        /*factor1*/       883.74,
        /*rate1*/        1500.00,
        /*threshold2*/  52151.00,
        /*factor2*/       228.74,
        /*rate2*/        2397.00,
        /*constant2*/     989.00,
        /*threshold3*/ 250000.00,
        /*rate3*/           0.42,
        /*constant3*/    7914.00,
        /*rate4*/           0.45,
        /*constant4*/   15414.00
    ],
    2009: [
        /*exemption*/    7834.00,
        /*threshold1*/  13139.00,
        /*factor1*/       939.68,
        /*rate1*/        1400.00,
        /*threshold2*/  52551.00,
        /*factor2*/       228.74,
        /*rate2*/        2397.00,
        /*constant2*/    1007.00,
        /*threshold3*/ 250400.00,
        /*rate3*/           0.42,
        /*constant3*/    8064.00,
        /*rate4*/           0.45,
        /*constant4*/   15576.00
    ],
    2010: [
        /*exemption*/    8004.00,
        /*threshold1*/  13469.00,
        /*factor1*/       912.17,
        /*rate1*/        1400.00,
        /*threshold2*/  52881.00,
        /*factor2*/       228.74,
        /*rate2*/        2397.00,
        /*constant2*/    1038.00,
        /*threshold3*/ 250730.00,
        /*rate3*/           0.42,
        /*constant3*/    8172.00,
        /*rate4*/           0.45,
        /*constant4*/   15694.00
    ],
    2013: [
        /*exemption*/    8130.00,
        /*threshold1*/  13469.00,
        /*factor1*/       933.70,
        /*rate1*/        1400.00,
        /*threshold2*/  52881.00,
        /*factor2*/       228.74,
        /*rate2*/        2397.00,
        /*constant2*/    1014.00,
        /*threshold3*/ 250730.00,
        /*rate3*/           0.42,
        /*constant3*/    8196.00,
        /*rate4*/           0.45,
        /*constant4*/   15718.00
    ],
    2014: [
        /*exemption*/    8354.00,
        /*threshold1*/  13469.00,
        /*factor1*/       974.58,
        /*rate1*/        1400.00,
        /*threshold2*/  52881.00,
        /*factor2*/       228.74,
        /*rate2*/        2397.00,
        /*constant2*/     971.00,
        /*threshold3*/ 250730.00,
        /*rate3*/           0.42,
        /*constant3*/    8239.00,
        /*rate4*/           0.45,
        /*constant4*/   15761.00
    ],
    2015: [
        /*exemption*/    8472.00,
        /*threshold1*/  13469.00,
        /*factor1*/       997.60,
        /*rate1*/        1400.00,
        /*threshold2*/  52881.00,
        /*factor2*/       228.74,
        /*rate2*/        2397.00,
        /*constant2*/     948.68,
        /*threshold3*/ 250730.00,
        /*rate3*/           0.42,
        /*constant3*/    8261.29,
        /*rate4*/           0.45,
        /*constant4*/   15783.19
    ],
    2016: [
        /*exemption*/    8652.00,
        /*threshold1*/  13669.00,
        /*factor1*/       993.62,
        /*rate1*/        1400.00,
        /*threshold2*/  53665.00,
        /*factor2*/       225.40,
        /*rate2*/        2397.00,
        /*constant2*/     952.48,
        /*threshold3*/ 254446.00,
        /*rate3*/           0.42,
        /*constant3*/    8394.14,
        /*rate4*/           0.45,
        /*constant4*/   16027.52
    ],
    2017: [
        /*exemption*/    8820.00,
        /*threshold1*/  13769.00,
        /*factor1*/      1007.27,
        /*rate1*/        1400.00,
        /*threshold2*/  54057.00,
        /*factor2*/       223.76,
        /*rate2*/        2397.00,
        /*constant2*/     939.57,
        /*threshold3*/ 256303.00,
        /*rate3*/           0.42,
        /*constant3*/    8475.44,
        /*rate4*/           0.45,
        /*constant4*/   16164.53
    ],
    2018: [
        /*exemption*/    9000.00,
        /*threshold1*/  13996.00,
        /*factor1*/       997.80,
        /*rate1*/        1400.00,
        /*threshold2*/  54949.00,
        /*factor2*/       220.13,
        /*rate2*/        2397.00,
        /*constant2*/     948.49,
        /*threshold3*/ 260532.00,
        /*rate3*/           0.42,
        /*constant3*/    8621.75,
        /*rate4*/           0.45,
        /*constant4*/   16437.70
    ],
    2019: [
        /*exemption*/    9168.00,
        /*threshold1*/  14254.00,
        /*factor1*/       980.14,
        /*rate1*/        1400.00,
        /*threshold2*/  55960.00,
        /*factor2*/       216.16,
        /*rate2*/        2397.00,
        /*constant2*/     965.58,
        /*threshold3*/ 265326.00,
        /*rate3*/           0.42,
        /*constant3*/    8780.90,
        /*rate4*/           0.45,
        /*constant4*/   16740.68
    ],
    2020: [
        /*exemption*/    9408.00,
        /*threshold1*/  14532.00,
        /*factor1*/       972.87,
        /*rate1*/        1400.00,
        /*threshold2*/  57051.00,
        /*factor2*/       212.02,
        /*rate2*/        2397.00,
        /*constant2*/     972.79,
        /*threshold3*/ 270500.00,
        /*rate3*/           0.42,
        /*constant3*/    8963.74,
        /*rate4*/           0.45,
        /*constant4*/   17078.74
    ]
};