Rows from data

Rows that come from data (line items, corrections, allowances), however many there are, as one line that folds: a payslip's corrections, an invoice's surcharges, a team's movements.

import { Sum, Plus, Minus } from "@umriss-ui/calculation";

A Sum takes the rows of an array as they come: many, one, or none. With one it still folds and opens onto that one; with none it is worth zero, has nothing to open, and says "no entries" where its formula would stand. The line keeps its place either way, so the statement does not change shape with the data (ADR-0049).

Rows of both signs go into the sum as they come. A line that adds or takes away shows its contribution: the direction as its operator, the number without a sign – "− Overpaid travel 120.00", never "+ -120.00". The quantity itself keeps its sign where its derivation closes, as an interim, as the result and wherever it is referenced. A factor keeps its sign; a line worth zero or missing keeps the operator it is written with.

Plus and Minus say how a quantity enters, the number its sign. Signed rows from data go into Plus; a quantity that is positive by nature and taken away – income tax, a sum of contributions – into Minus, so that it is never quoted as negative.

Examples

Rows of any count

Hand a Sum the rows of an array, as they come from the data, and hold it in a Plus or Minus: the rows fold into one line, and the interim after it takes their total.

import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation";

/* Data from a payroll system, written out here so the example runs on its own. */
const ALLOWANCES = [
  { id: "night", name: "Night work bonus", amount: 84 },
  { id: "holiday", name: "Holiday pay", amount: 120 },
  { id: "on-call", name: "On-call allowance", amount: 45 },
];

export default function RowsOfAnyCount() {
  return (
    <Calculation aria-label="Payslip, March">
      <Chain>
        <Given label="Gross salary" value={4200} unit="€" decimals={2} />
        <Plus>
          <Sum label="Allowances" unit="€" decimals={2}>
            {ALLOWANCES.map((allowance) => (
              <Given key={allowance.id} label={allowance.name} value={allowance.amount} unit="€" decimals={2} />
            ))}
          </Sum>
        </Plus>
        <Minus label="Income tax" value={668.4} unit="€" decimals={2} />
        <Interim label="Net salary" unit="€" decimals={2} />
      </Chain>
    </Calculation>
  );
}

No rows

When the data holds no rows, the sum is zero and its line stays where it was: it has nothing to open and says "no entries" where its formula would stand, so a reader sees that there were none rather than wondering whether a line is missing.

import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation";

/* A month without allowances: the payroll system returns an empty list. */
const ALLOWANCES: readonly { id: string; name: string; amount: number }[] = [];

export default function NoRows() {
  return (
    <Calculation aria-label="Payslip, April">
      <Chain>
        <Given label="Gross salary" value={4200} unit="€" decimals={2} />
        <Plus>
          <Sum label="Allowances" unit="€" decimals={2}>
            {ALLOWANCES.map((allowance) => (
              <Given key={allowance.id} label={allowance.name} value={allowance.amount} unit="€" decimals={2} />
            ))}
          </Sum>
        </Plus>
        <Minus label="Income tax" value={612.5} unit="€" decimals={2} />
        <Interim label="Net salary" unit="€" decimals={2} />
      </Chain>
    </Calculation>
  );
}

One row

A sum of one row is still a group: its line keeps its name and folds, and opens onto that one row, closing with "= Allowances". The statement keeps its shape as the data changes, and an opened group stays open.

import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation";

/* A month with a single allowance. */
const ALLOWANCES = [{ id: "night", name: "Night work bonus", amount: 84 }];

export default function OneRow() {
  return (
    <Calculation aria-label="Payslip, May">
      <Chain>
        <Given label="Gross salary" value={4200} unit="€" decimals={2} />
        <Plus>
          <Sum label="Allowances" unit="€" decimals={2}>
            {ALLOWANCES.map((allowance) => (
              <Given key={allowance.id} label={allowance.name} value={allowance.amount} unit="€" decimals={2} />
            ))}
          </Sum>
        </Plus>
        <Minus label="Income tax" value={631.9} unit="€" decimals={2} />
        <Interim label="Net salary" unit="€" decimals={2} />
      </Chain>
    </Calculation>
  );
}

Rows of both signs

Rows that add and rows that take away go into one Sum as they come, each with its own sign. Every line shows its contribution: the direction as its operator, the number without a sign – a correction of −120 stands as "− 120.00", never as "+ -120.00".

import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation";

/* Corrections from a payroll system: positive adds to the pay, negative takes from it. */
const CORRECTIONS = [
  { id: "night", name: "Night work bonus, February", amount: 84 },
  { id: "travel", name: "Overpaid travel, February", amount: -120 },
  { id: "rounding", name: "Rounding", amount: 0.03 },
];

export default function RowsOfBothSigns() {
  return (
    <Calculation aria-label="Payslip, March">
      <Chain>
        <Given label="Gross salary" value={4200} unit="€" decimals={2} />
        <Minus label="Income tax" value={612.5} unit="€" decimals={2} />
        <Plus>
          <Sum label="Corrections" unit="€" decimals={2}>
            {CORRECTIONS.map((correction) => (
              <Given key={correction.id} label={correction.name} value={correction.amount} unit="€" decimals={2} />
            ))}
          </Sum>
        </Plus>
        <Interim label="Net salary" unit="€" decimals={2} />
      </Chain>
    </Calculation>
  );
}

A group that turns negative

Held by a Plus, a group whose total is negative stands as a minus with its number unsigned: "− Corrections 36.00". Opened, its derivation closes with the quantity itself, sign and all: "= Corrections -36.00". The line says what it does to the pay, the closing row what the corrections amount to.

import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation";

const CORRECTIONS = [
  { id: "night", name: "Night work bonus, February", amount: 84 },
  { id: "travel", name: "Overpaid travel, February", amount: -120 },
];

export default function AGroupThatTurnsNegative() {
  return (
    <Calculation aria-label="Payslip, March">
      <Chain>
        <Given label="Gross salary" value={4200} unit="€" decimals={2} />
        <Minus label="Income tax" value={612.5} unit="€" decimals={2} />
        <Plus>
          <Sum label="Corrections" unit="€" decimals={2}>
            {CORRECTIONS.map((correction) => (
              <Given key={correction.id} label={correction.name} value={correction.amount} unit="€" decimals={2} />
            ))}
          </Sum>
        </Plus>
        <Interim label="Net salary" unit="€" decimals={2} />
      </Chain>
    </Calculation>
  );
}

Plus or Minus

Plus and Minus say how a quantity enters, the number says its sign. Signed rows from data go into Plus: their signs do the rest. A quantity that is positive by nature and taken away here – income tax, a sum of contributions – goes into Minus, so that it keeps its sign where it closes and wherever it is referenced.

import { Calculation, Chain, Given, Interim, Minus, Plus, Product, Ref, Sum } from "@umriss-ui/calculation";

const CONTRIBUTIONS = [
  { name: "Pension insurance", rate: 0.093 },
  { name: "Health insurance", rate: 0.082 },
];

const CORRECTIONS = [
  { id: "night", name: "Night work bonus, February", amount: 84 },
  { id: "travel", name: "Overpaid travel, February", amount: -120 },
];

export default function PlusOrMinus() {
  return (
    <Calculation aria-label="Payslip, March">
      <Chain>
        <Given id="gross" label="Gross salary" value={4200} unit="€" decimals={2} />
        <Minus label="Income tax" value={612.5} unit="€" decimals={2} />
        <Minus>
          <Sum label="Social security contributions" unit="€" decimals={2}>
            {CONTRIBUTIONS.map((contribution) => (
              <Product key={contribution.name} label={contribution.name} unit="€" decimals={2}>
                <Given label={`Rate, ${contribution.name.toLowerCase()}`} value={contribution.rate} format="percent" />
                <Ref to="gross" />
              </Product>
            ))}
          </Sum>
        </Minus>
        <Plus>
          <Sum label="Corrections" unit="€" decimals={2}>
            {CORRECTIONS.map((correction) => (
              <Given key={correction.id} label={correction.name} value={correction.amount} unit="€" decimals={2} />
            ))}
          </Sum>
        </Plus>
        <Interim label="Net salary" unit="€" decimals={2} />
      </Chain>
    </Calculation>
  );
}

A deduction first

The first operand of a sum stands without an operator – unless it lowers the sum: then it carries a minus like every other deduction. A folded line's formula starts with that minus too – "A sum from data in a tree" below shows one.

import { Calculation, Given, Sum } from "@umriss-ui/calculation";

const ROWS = [
  { id: "refund", name: "Refund, order 1184", amount: -30 },
  { id: "bonus", name: "Loyalty bonus", amount: 12 },
  { id: "voucher", name: "Voucher", amount: 5 },
];

export default function ADeductionFirst() {
  return (
    <Calculation aria-label="Customer account, March">
      <Sum label="Balance" unit="€" decimals={2}>
        {ROWS.map((row) => (
          <Given key={row.id} label={row.name} value={row.amount} unit="€" decimals={2} />
        ))}
      </Sum>
    </Calculation>
  );
}

Zero and missing keep their sign

A row worth zero has no direction, and a missing row has no number: both keep the operator they are written with. The missing row makes the group and everything after it absent, with the reason – never zero.

import { Calculation, Chain, Given, Interim, Minus, Plus, Sum } from "@umriss-ui/calculation";

const CORRECTIONS: readonly { id: string; name: string; amount: number | null }[] = [
  { id: "night", name: "Night work bonus, February", amount: 84 },
  { id: "travel", name: "Travel, February", amount: 0 },
  { id: "overtime", name: "Overtime, February", amount: null },
];

export default function ZeroAndMissingKeepTheirSign() {
  return (
    <Calculation aria-label="Payslip, March">
      <Chain>
        <Given label="Gross salary" value={4200} unit="€" decimals={2} />
        <Minus label="Income tax" value={612.5} unit="€" decimals={2} />
        <Plus>
          <Sum label="Corrections" unit="€" decimals={2}>
            {CORRECTIONS.map((correction) => (
              <Given key={correction.id} label={correction.name} value={correction.amount} unit="€" decimals={2} />
            ))}
          </Sum>
        </Plus>
        <Interim label="Net salary" unit="€" decimals={2} />
      </Chain>
    </Calculation>
  );
}

A sum from data in a tree

Rows from data work in a tree as in a chain: here the stock movements of a day, whatever their count and sign, stand as one operand of a Difference. Taken away, a negative total turns into a plus – "+ Movements 7" – and the derivation closes with its own sign.

import { Calculation, Difference, Given, Sum } from "@umriss-ui/calculation";

const MOVEMENTS = [
  { id: "m1", name: "Picked, order 5512", amount: -12 },
  { id: "m2", name: "Received, delivery 881", amount: 40 },
  { id: "m3", name: "Picked, order 5517", amount: -35 },
];

export default function ASumFromDataInATree() {
  return (
    <Calculation aria-label="Shelf B-14, 17 March">
      <Difference label="Gap to plan" unit="pcs">
        <Given label="Planned stock" value={150} unit="pcs" />
        <Given label="Stock this morning" value={142} unit="pcs" />
        <Sum label="Movements" unit="pcs">
          {MOVEMENTS.map((movement) => (
            <Given key={movement.id} label={movement.name} value={movement.amount} unit="pcs" />
          ))}
        </Sum>
      </Difference>
    </Calculation>
  );
}

Rows of both signs with metrics

With metrics a line has one operator and a number per metric. Where they all point the same way, the line shows its contribution as everywhere else; a metric that is zero or missing does not count. Where they point in different directions – a part-timer joins: one more head, half a position less – the line keeps its written operator and every number its sign.

import { Calculation, Chain, Given, Interim, Plus, Sum } from "@umriss-ui/calculation";
import type { Metric } from "@umriss-ui/calculation";

const STAFF: readonly Metric[] = [
  { id: "heads", label: "Headcount", unit: "HC", decimals: 0 },
  { id: "fte", label: "Full-time equivalents", unit: "FTE", decimals: 1 },
];

/* The quarter's movements from the HR system. */
const MOVEMENTS = [
  { id: "sales", name: "Transferred to sales", heads: -4, fte: -3.5 },
  { id: "hours", name: "Hours reduced", heads: 0, fte: -1.5 },
  { id: "part-time", name: "Full-timer replaced by two part-timers", heads: 1, fte: -0.5 },
  { id: "joiners", name: "Joiners", heads: 6, fte: 5.8 },
];

export default function RowsOfBothSignsWithMetrics() {
  return (
    <Calculation aria-label="Staff movement, logistics, third quarter" metrics={STAFF}>
      <Chain>
        <Given label="Staff on 30 June" value={{ heads: 64, fte: 48.9 }} />
        <Plus>
          <Sum label="Movements">
            {MOVEMENTS.map((movement) => (
              <Given key={movement.id} label={movement.name} value={{ heads: movement.heads, fte: movement.fte }} />
            ))}
          </Sum>
        </Plus>
        <Interim label="Staff on 30 September" />
      </Chain>
    </Calculation>
  );
}

When to use something else

Keyboard

The keys of Calculation apply here.

Props on this page

From CalculationProps — the full table stands on Calculation.

NameTypeDefaultDescription
childrenrequiredReactNode—The result: exactly one quantity - a tree (<Sum>, <Difference>, <Product>, <Quotient> with their operands, <Given> as leaves) or a <Chain>, the two mixing freely; <Ref> for a quantity defined elsewhere.
metricsreadonly Metric[]—Several numbers per quantity, side by side - headcount and full-time equivalents of the same teams (ADR-0038). Every value is then an object with a number for each metric id; unit and places are the metric's. A calculation with metrics only adds and subtracts, and assesses no target or limits.

From OperatorProps — the full table stands on Tree.

NameTypeDefaultDescription
childrenReactNode—The operands, in order: quantities, givens and references.
labelrequiredstring—What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. from QuantityProps
idstring—Makes the quantity referable: <Ref to> stands for it elsewhere. Needed only for a quantity used more than once. from QuantityProps
unitstring—A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. from QuantityProps
format"percent"—percent presents a plain ratio as per cent: 0.916 is shown as 91.6 %. The quantity stays a ratio in every operation. from QuantityProps
decimalsnumberone digit for a percentage, at most two for a derived number, a given as it was givenFixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. from QuantityProps

From RefProps — the full table stands on Tree.

NameTypeDefaultDescription
torequiredstring—The id of the quantity this reference stands for.

From ChainProps — the full table stands on Chain.

NameTypeDefaultDescription
childrenReactNode—The operands, top to bottom: a first quantity with no operator, then Plus, Minus, Times and DividedBy with their operands and Interims naming the value where they stand. The last line is an Interim; a Times or DividedBy stands alone between two named values.

From ChainOperandProps — the full table stands on Chain.

NameTypeDefaultDescription
labelstring—The operand's name, where the line is a given written in place. Left out where the line holds a quantity as its child.
value| number
| null
| MetricValues
—The operand's number, where the line is a given written in place. Absent as for a Given: the line and every interim after it are absent, with the reason. With metrics, an object by metric id, as on a Given.
childrenReactNode—Instead of label and value: exactly one quantity - a tree, a <Ref> or a <Chain>.
idstring—Makes the quantity referable: <Ref to> stands for it elsewhere. Needed only for a quantity used more than once. from QuantityProps
unitstring—A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. from QuantityProps
format"percent"—percent presents a plain ratio as per cent: 0.916 is shown as 91.6 %. The quantity stays a ratio in every operation. from QuantityProps
decimalsnumberone digit for a percentage, at most two for a derived number, a given as it was givenFixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. from QuantityProps

From QuantityProps — the full table stands on Chain.

NameTypeDefaultDescription
labelrequiredstring—What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence.
idstring—Makes the quantity referable: <Ref to> stands for it elsewhere. Needed only for a quantity used more than once.
unitstring—A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra.
format"percent"—percent presents a plain ratio as per cent: 0.916 is shown as 91.6 %. The quantity stays a ratio in every operation.
decimalsnumberone digit for a percentage, at most two for a derived number, a given as it was givenFixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value.

From GivenProps — the full table stands on Given.

NameTypeDefaultDescription
valuerequired| number
| null
| undefined
| MetricValues
—The number the calculation starts from. Absent (null, undefined, not finite) makes every quantity that depends on it absent, with the reason - never zero. Where the calculation has metrics, an object with a number for each metric id instead; absence then stays in its metric.
labelrequiredstring—What the quantity is called. It stands on its line, wherever it is an operand, and in the accessible sentence. from QuantityProps
idstring—Makes the quantity referable: <Ref to> stands for it elsewhere. Needed only for a quantity used more than once. from QuantityProps
unitstring—A label written after the number - "min", "pcs", "€". Never converted and never checked; the calculation does no unit algebra. from QuantityProps
format"percent"—percent presents a plain ratio as per cent: 0.916 is shown as 91.6 %. The quantity stays a ratio in every operation. from QuantityProps
decimalsnumberone digit for a percentage, at most two for a derived number, a given as it was givenFixed number of fraction digits as shown. Rounding happens only in presentation; every operation uses the full value. from QuantityProps

From Metric — the full table stands on Metrics.

NameTypeDefaultDescription
idrequiredstring—The key of this metric's number in every value object.
labelrequiredstring—The metric's name, in the head above its column and in each row's sentence.
unitstring—A label written after the number where a result closes, and beneath the metric's name in the head. Never converted and never checked.
decimalsnumbera given as it was given, at most two for a derived numberFixed number of fraction digits as shown, for every quantity of this metric.

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/calculation

Getting started

Writing a calculation

In practice

API index