The Problem

Every payroll system processes periods — monthly, bi-weekly, weekly. The natural assumption is that one period equals one set of inputs and one set of outputs. But real employment is continuous, and changes rarely wait for month boundaries. An employee gets a raise effective March 15. Another switches tax classes on June 20. A third joins the company on April 10 and is promoted on April 22. Each of these events fractures the “atomic” period into sub-periods that must be calculated independently and then reassembled into a single payslip.

This isn’t merely a prorating problem. If salary were the only variable, you could split the month by calendar days and be done. But payroll formulas are non-linear. Tax calculations use progressive brackets. Social security contributions hit ceilings. Allowances apply per-period, not per-day. A weighted average of the inputs fed through a non-linear formula produces a different result than the sum of the formula applied to each sub-period independently.

The arithmetic trap: An employee earns 3,000 EUR from Jan 1–14 and 4,000 EUR from Jan 15–31. The naive approach averages the salary to 3,548 EUR (weighted by days) and runs the tax calculation once. The correct approach calculates tax on 1,400 EUR (14/31 × 3,000) and tax on 2,194 EUR (17/31 × 4,000) separately, then sums. With progressive tax brackets, these two results diverge — sometimes by hundreds of euros per month.

The problem compounds with multiple simultaneous changes. If salary changes on the 10th and the tax class changes on the 20th, you now have three sub-periods: days 1–9 (old salary, old tax class), days 10–19 (new salary, old tax class), and days 20–31 (new salary, new tax class). Each sub-period is a distinct calculation context with its own parameter set. The payslip must show one net amount, but internally the engine has executed three separate calculation passes.

And then there are the formulas that involve two scaled values multiplied together. When both the salary and a rate are prorated by days, the product is scaled twice — once for each operand. A salary of 3,000 EUR prorated to 17/31 days is 1,645.16 EUR. A contribution rate of 7.3% prorated to 17/31 days is 4.0% (as a scaled value). Multiply them and you get 65.81 EUR — but the correct answer is 120.10 EUR (1,645.16 × 0.073). The scale factor has been applied twice, reducing the result by a factor of 17/31. This is the CalendarPeriod × CalendarPeriod trap, and it is one of the most subtle bugs in payroll arithmetic.

How It Works

Mid-month changes are handled differently in every country, but the underlying mechanics fall into a few recognizable patterns. The core question is always: how does the legal framework define “one period’s worth” of a value that changed partway through?

Single Mid-Month Change

The simplest case: one value changes on one date within a period. Consider an employee in Germany earning 4,000 EUR/month whose salary increases to 4,500 EUR effective March 16, in a 31-day month.

The gross pay splits into two sub-periods:

  • Sub-period A (Mar 1–15, 15 days): 4,000 × 15/31 = 1,935.48 EUR
  • Sub-period B (Mar 16–31, 16 days): 4,500 × 16/31 = 2,322.58 EUR
  • Total gross: 4,258.06 EUR

Each sub-period’s gross is then fed independently into the income tax algorithm (the PAP in Germany), yielding two separate tax amounts. Social security contributions apply their own ceilings to each sub-period’s base. The payslip shows one line for gross, one for tax, one for each contribution — but behind each line is the sum of two independent calculations.

Country Comparison

The legal basis for prorating varies significantly:

Country Legal Basis Proration Method Typical Divisor
DE § 39b Abs. 2 EStG, SvEV Calendar days in the period 28–31 (actual month days)
FR Art. L3141-3 Code du travail, Cass. soc. Working days (jours ouvrés) or calendar days depending on convention 21.67 (avg working days) or calendar days
UK HMRC PAYE guidelines, ERA 1996 § 2 Calendar days in the tax period 365/12 or actual period days
ES Art. 26.4 ET, Convenio Colectivo Calendar days; Convenio may override 30 (standard) or actual days
US IRS Pub 15-T, FLSA § 7 Pay periods per year; mid-period uses partial-period fraction 12 (monthly), 24 (semi-monthly), 26 (bi-weekly)
PT Art. 270–271 Código do Trabalho Calendar days in the month 30 (standard) or actual days

France is particularly interesting because the proration method depends on the applicable convention collective. Some industries use jours ouvrés (working days, typically 21.67/month), others use jours ouvrables (6-day weeks), and some use calendar days. The same mid-month salary change can yield three different gross amounts depending on which convention applies.

Multiple Changes in One Period

When two or more values change on different dates within a single period, the month fragments into N+1 sub-periods for N change dates. Each sub-period inherits the full parameter context from the preceding sub-period, modified by the specific change that created the boundary.

Example: US employee, monthly pay period, March 2026:

  • March 1: Salary $6,000/month, filing status Single, work state New York
  • March 10: Salary increases to $7,200/month
  • March 20: Employee transfers to California

This creates three sub-periods:

Sub-Period Days Salary State Prorated Gross
Mar 1–9 9 $6,000 NY $1,741.94
Mar 10–19 10 $7,200 NY $2,322.58
Mar 20–31 12 $7,200 CA $2,787.10

Federal income tax is calculated on the total gross ($6,851.62), but state income tax must split: sub-periods 1 and 2 are subject to New York state tax, sub-period 3 to California state tax. The FICA calculation (Social Security + Medicare) runs on total gross, but state disability insurance (SDI) applies only to the California portion. You cannot collapse these sub-periods without losing the jurisdictional boundary.

Prorata Entry and Exit

The most common mid-month change is the simplest: an employee joins or leaves partway through a period. A new hire starting on March 12 has a 20-day sub-period (Mar 12–31) and zero for the preceding days. An employee departing on March 18 has an 18-day sub-period and nothing after.

In Portugal, prorata entry interacts with the subsídio de refeição (meal allowance), which is paid per working day actually worked. An employee starting mid-month doesn’t receive half the monthly meal allowance — they receive the daily rate times the number of actual working days from their start date through month end. This is a per-day benefit, not a prorated monthly benefit, and the distinction matters for both the gross calculation and the tax-exempt threshold (currently 6.00 EUR/day for card payments, per Portaria n.º 107-A/2023).

Where It Gets Tricky

The CalendarPeriod × CalendarPeriod Scaling Trap

This is the subtlest and most dangerous bug in time-segmented payroll. It occurs when two values that are both distributed across the period (i.e., prorated by calendar days) are multiplied together.

Consider a contribution formula: contribution = salary × rate. Both salary and rate are monthly values. When a change occurs mid-month, both are prorated:

  • Salary for sub-period: 4,000 × 16/31 = 2,064.52
  • Rate for sub-period: 0.073 × 16/31 = 0.03768
  • Product: 2,064.52 × 0.03768 = 77.79 EUR

But the correct answer is:

  • Salary for sub-period: 4,000 × 16/31 = 2,064.52
  • Rate (not prorated — it’s a factor, not a distributed amount): 0.073
  • Product: 2,064.52 × 0.073 = 150.71 EUR

The difference is 72.92 EUR — nearly half the correct value. The scaling factor (16/31) was applied to both operands, so the product contains it squared: (16/31)² instead of 16/31.

The rule: In any multiplication involving time-segmented values, at most one operand may be a distributed (calendar-period-scaled) value. The other must be a factor — a value that applies “from this date forward” without being prorated by days. Salary is distributed. A contribution rate is a factor. A tax bracket threshold is neither — it’s a fixed parameter for the entire period.

This trap is invisible in testing when changes happen only on period boundaries (the scale factor is 1.0, so squaring it has no effect). It only manifests in production when actual mid-month changes occur — exactly the scenario you cannot afford to get wrong.

Why Collapsing Segments Fails

A common shortcut in payroll implementations is to “collapse” all segments of a case value into a single weighted average before performing calculations. If an employee earned 3,000 EUR for 15 days and 4,000 EUR for 16 days, collapse it to a single number (e.g., 3,548.39 EUR) and calculate once.

This works for linear operations: a flat-rate contribution on a collapsed value equals the sum of flat-rate contributions on each sub-period. But it fails for every non-linear operation:

  • Progressive tax brackets: Tax on 3,548 EUR ≠ tax on 1,452 EUR + tax on 2,065 EUR. The bracket boundaries hit differently on each sub-period.
  • Contribution ceilings: If the annual social security ceiling prorates to 4,237.50 EUR/month in Germany (2026 KV BBG: 66,150 EUR/year), a collapsed salary of 3,548 EUR is below the ceiling. But the second sub-period’s annualized salary (4,000 × 12 = 48,000) might interact differently with the ceiling than the first sub-period’s (3,000 × 12 = 36,000).
  • Allowance thresholds: Some allowances (like the German Arbeitnehmer-Pauschbetrag or the UK personal allowance) apply once per period, not proportionally per sub-period. Collapsing erases the information needed to allocate them correctly.

Overlapping Dimensions

The hardest mid-month scenarios involve changes across multiple independent dimensions simultaneously. Each dimension creates its own set of sub-period boundaries, and the intersection of all dimensions defines the actual sub-periods the engine must calculate.

Consider a French employee in March 2026:

  • March 8: Salary changes from 3,200 EUR to 3,600 EUR
  • March 15: The applicable convention collective changes the employer’s supplementary pension rate from 4.72% to 5.10%
  • March 22: The employee moves from working 100% to 80% (temps partiel)

Three change dates create four sub-periods, each with a unique combination of salary, pension rate, and working-time percentage. The gross calculation for each sub-period differs. The PAS (prélèvement à la source) calculation uses the total monthly salary but must account for the part-time transition on March 22. CSG/CRDS bases are different for each sub-period because the employer pension contribution — itself different in each sub-period — is partially deductible.

This is not a theoretical exercise. In France, convention collective rate changes are published by industry bodies and can take effect on any date. Working-time changes (passage à temps partiel) are frequently mid-month. Salary revisions tied to seniority thresholds fall on anniversary dates. A payroll system that cannot handle arbitrary sub-period boundaries will produce incorrect payslips for a meaningful percentage of the French workforce every month.

The Annualization Problem

Several countries calculate income tax by annualizing the period income, looking up the annual tax, and de-annualizing back to the period. Germany’s PAP (Programmablaufplan) does exactly this. So does the UK’s PAYE cumulative method and the US federal withholding under the percentage method (IRS Pub 15-T).

When a mid-month change occurs, which salary do you annualize? The sub-period salary prorated to a month? The actual sub-period amount? The full monthly salary that would have applied if the change hadn’t happened?

The answer depends on the country:

  • Germany: Each sub-period’s prorated gross is annualized independently. The PAP runs on each annualized amount, producing an annual tax, which is then de-annualized and prorated back to the sub-period’s day fraction.
  • UK: The cumulative method uses year-to-date totals. A mid-month change doesn’t split the period — it affects the running YTD total, and the tax calculation operates on the cumulative basis. The “Month 1” (non-cumulative) method, used for emergency tax codes, does not split either.
  • US: Annualization uses the number of pay periods per year. A mid-period salary change prorates the period income, which is then multiplied by the annual pay period count. Sub-period tax amounts are summed.

Getting the annualization wrong by even a few euros in one sub-period can cascade through the year’s cumulative calculations, creating a growing discrepancy that only surfaces during year-end reconciliation or a retroactive correction.

How PE Solves It

Payroll Engine’s approach to mid-month changes is built on a fundamental design principle: case values carry their time history. When an employee’s salary changes, the system doesn’t overwrite the old value. It stores a new value with an effective date, and the old value remains accessible for any period that predates the change. During payrun execution, the engine reads all values that overlap the current period and automatically segments the calculation.

Time Types: Period vs. CalendarPeriod vs. Timeless

Every case field in PE has a time type that determines how the value behaves when a mid-period change exists:

Time Type Behavior at Read Typical Use
CalendarPeriod Prorated by calendar days — value × subDays / periodDays Salary, allowances, monthly amounts
Period Full value from effective date — no proration Rates, factors, tariffs, tax class
Timeless Single value regardless of date — no segmentation Contract-fixed values, identifiers

This distinction directly solves the CalendarPeriod × CalendarPeriod trap. Salary is CalendarPeriod (distributed across the month). A contribution rate is Period (applies from a date, not distributed). When the engine reads both values for a sub-period, the salary is prorated and the rate is not. The multiplication produces the correct result without any special-case logic in the formula.

CasePayrollValue Arithmetic

The engine’s core innovation is the CasePayrollValue type — a value that carries its time segments internally. When a wage type formula requests multiple case values that have different change dates, the engine aligns them into a common sub-period grid and performs arithmetic segment by segment.

The key API pattern is GetCaseValues(field1, field2, ...), which returns a dictionary of CasePayrollValues aligned to the same sub-period boundaries. Arithmetic operators on these values work per-segment automatically:

  • values["Salary"] * values["ContributionRate"] multiplies each sub-period’s salary by each sub-period’s rate
  • The result is itself a CasePayrollValue with the same sub-period structure
  • Converting to a scalar (for the wage type result) sums all sub-period amounts

This is fundamentally different from the collapse-then-calculate approach. The formula author writes a single expression — salary * rate — and the engine handles the sub-period decomposition, alignment, independent calculation, and reassembly transparently.

The GetCaseValues vs. GetCaseValue Distinction

PE offers two APIs for reading case values, and choosing the wrong one is the primary source of mid-month calculation errors:

  • GetCaseValue<T>(field) — collapses all segments into a single scalar. For CalendarPeriod fields, this is the weighted sum. For Period fields, this is the value at the evaluation date. Use this when you need a single number and the formula is not multiplying two period-sensitive values.
  • GetCaseValues(field1, field2, ...) — preserves all segments and returns CasePayrollValues. Use this whenever the formula involves multiplication or division of two or more period-sensitive values.

The distinction matters because GetCaseValue loses information. Once segments are collapsed, you cannot recover the per-sub-period breakdown. If you collapse salary to 3,548 EUR and then multiply by a contribution rate, you’ve already destroyed the sub-period boundary information that determines which rate applies to which portion of the salary.

Sub-Period Grid Assembly

When the engine encounters a period with mid-month changes, it builds a sub-period grid from all case values that have changes within the period. The process is automatic and transparent to the formula author:

  1. Collect change dates: Scan all case values referenced by the current wage type for effective dates within the payrun period.
  2. Build boundaries: Sort all unique change dates. Each pair of adjacent dates defines a sub-period.
  3. Resolve values: For each sub-period and each case field, determine the applicable value based on the field’s time type.
  4. Execute: Run the wage type formula once per sub-period with the resolved values.
  5. Aggregate: Sum the sub-period results to produce the period result.

If salary changes on March 10 and the tax class changes on March 20, the grid has three sub-periods. Each wage type in the calculation chain receives the same three sub-periods, ensuring consistency across gross, tax, social security, and net calculations.

Data Regulations and Threshold Versioning

Some parameters change not because of an employee event but because the law changes mid-year. When a country adjusts a social security ceiling or introduces a new tax bracket effective July 1, that change must be applied to all payrolls from July onward — potentially splitting July into two sub-periods if employees have other changes in that month.

PE handles this through data regulations — versioned parameter sets with effective dates. Each annual data regulation (e.g., Data.LSt.2026, Data.SV.2026) carries its own validFrom date. When a mid-year legislative change occurs, a new data regulation version with an updated validFrom takes effect. The engine’s sub-period logic treats regulatory parameter changes the same way it treats employee data changes: as boundary events that may create new sub-periods.

This architecture means the same payrun execution logic handles all three sources of mid-period complexity: employee changes (salary, address, tax class), regulatory changes (new rates, new ceilings), and event-driven changes (entry, exit, leave). No special-case branching, no separate code paths.

Test Case References

The following integration tests validate the scenarios described in this article:

TestCountryScenario
WT-TC-IP1 US Single mid-period salary change — federal and state withholding split
WT-TC-IP2 US Mid-period salary change with state transfer (NY → CA)
WT-TC-IP3 US Multiple changes — salary and filing status in same period
WT-TC-IP4 US Three sub-periods with different state jurisdictions
WT-TC1000-FR-Intramonth-SalaireBase FR Mid-month salary change with PAS recalculation
WT-TC1000-FR-Intramonth-TauxPAS FR Mid-month PAS rate change (new tax notice)
WT-TC1000-FR-Intramonth-TempsPartiel FR Transition to part-time mid-month (temps partiel)
WT-TC1000-UK-Intramonth-SalaryChange UK Mid-month salary change with PAYE cumulative recalculation
WT-TC1100-PT-Intramonth-SubsidioRefeicao PT Meal allowance proration on mid-month entry
WT-TC10-DE-Prorata DE Prorata entry/exit with Lohnsteuer and SV split
WT-TC10-FR-Prorata FR Prorata entry with convention-specific day count
WT-TC10-UK-Prorata UK Prorata entry with Week 1/Month 1 PAYE basis
WT-TC10-ES-Prorata ES Prorata entry with IRPF and Seguridad Social split
WT-TC10-US-Prorata US Prorata entry with federal/state withholding and FICA
WT-TC10-PT-Prorata PT Prorata entry with IRS retention and TSU

See how PE handles this

Mid-month changes are the acid test for payroll engines. If your system collapses time segments, every non-linear formula is at risk. Payroll Engine’s CasePayrollValue arithmetic handles arbitrary sub-period boundaries across all 11 supported countries — with full test coverage for every scenario above.

Request a Demo →
← Back
All Articles