Date Difference
How far apart two dates are, in a unit you choose. Signed, and unrounded for fixed units.
Date Difference answers To minus From, counted in the chosen unit. It is signed — positive when To is later — so 'days until' and 'days since' are the same node read two ways, and Absolute drops the sign when only the distance matters. Fixed units divide exactly and are NOT rounded: 36 hours is 1.5 days. Months and years are counted in whole calendar steps, because a fractional month is not a quantity anyone can check; the answer is how many whole steps Date Add could take without overshooting, so 31 January to 28 February is 1 month and 31 January to 27 February is 0.
When to use it
Age, elapsed time, 'days remaining', SLA windows, deciding whether something is stale.
At a glance
| Category | Utilities |
| Type name | net.noodl.DateDifference |
| Available in | browser, cloud |
| SSR compatibility | safe |
| Provided by | noodl-runtime |
| Behaviour spec | version 3 (under the hood) |
Inputs
Values
| Name | Type | Default | Description |
|---|---|---|---|
absolute | Boolean | false | Drop the sign, so the output is a distance rather than a direction |
from | Date | — | The earlier instant, in the reading that makes Difference positive |
to | Date | — | The later instant. Difference is negative when this is actually earlier than From |
unit | Enum (milliseconds, seconds, minutes, hours, days, weeks, months, years) | days | What Difference counts. Fixed units are exact and fractional (36 hours is 1.5 days); months and years are whole calendar steps |
What each input does
From (from)
- From or To not given yet: kept for when both are; nothing is sent → Invalid Date, Changed stay silent
- either cannot be read as a date: Difference goes empty → Invalid Date; Changed stays silent
- otherwise Difference is To minus From in Unit (days when not one of the eight), negative when To is earlier unless Absolute is on → Changed; Invalid Date stays silent
To (to)
- From or To not given yet: kept for when both are; nothing is sent → Invalid Date, Changed stay silent
- either cannot be read as a date: Difference goes empty → Invalid Date; Changed stays silent
- otherwise Difference is To minus From in Unit (days when not one of the eight), negative when To is earlier unless Absolute is on → Changed; Invalid Date stays silent
Unit (unit)
- From or To not given yet: kept for when both are; nothing is sent → Invalid Date, Changed stay silent
- either cannot be read as a date: Difference goes empty → Invalid Date; Changed stays silent
- otherwise Difference is To minus From in Unit (days when not one of the eight), negative when To is earlier unless Absolute is on → Changed; Invalid Date stays silent
Absolute (absolute)
- From or To not given yet: kept for when both are; nothing is sent → Invalid Date, Changed stay silent
- either cannot be read as a date: Difference goes empty → Invalid Date; Changed stays silent
- otherwise Difference is To minus From in Unit (days when not one of the eight), negative when To is earlier unless Absolute is on → Changed; Invalid Date stays silent
Outputs
Values
| Name | Type | Default | Description |
|---|---|---|---|
difference | Number | — | To minus From, counted in Unit. Unset while either date is missing or unreadable |
Signals
| Name | Type | Default | Description |
|---|---|---|---|
changed | Signal | — | Fires after Difference has been recomputed |
Failure outputs
| Name | Type | Default | Description |
|---|---|---|---|
failure | Signal | — | Fires when a date arrived that could not be read, leaving Difference unset |
Patterns
- Record date → From, Now Date → To, unit Days: how old this record is.
Watch out for
- Assuming the result is a whole number. Fixed units are deliberately fractional; round it downstream if you need to.
Examples
How many days are left on this trial?
Bubble's <-range-> gives a range whose duration you then read; here the duration is the whole question, and Date Difference answers it directly as To minus From. Two things about that subtraction decide whether the banner is right. It is SIGNED — an expired trial reports a negative number, and that is the useful behaviour, because Absolute would turn 'three days overdue' into 'three days left'. And for fixed units it is NOT rounded: a trial with 36 hours to run reports 1.5 days, so a banner wired straight to Difference says '1.5 days left'. That is why the Expression is here rather than as an afterthought — Math.max(0, Math.ceil(d)) rounds up (a trial with any part of a day left has a day left, which is what a person counting means), and clamps the expired case to zero so the banner never counts downward past the end. Date Add supplies the end date from the signup date, and its clamping rule matters if you ever change Unit to Months: 31 January plus one month is 28 February, not 2 March. The end date is rendered through Date To String rather than pasted in as a number, because Difference is a number and a date is not.
Related nodes
Code export
Exports to React. A project that uses it exports this node as React code.
How export works shows what an export writes, with a worked example.
Under the hood
This node's behaviour is written down as an executable spec in @nodegx/node-spec, version 3. The spec is the reference the conformance tests grade the runtime against. Every rule in it cites the line of the runtime it was read from. The full spec is at the bottom of this page.
What it depends on outside the node
- the local time zone (
timezone)
Starting state
{
"fromSupplied": false,
"toSupplied": false,
"unit": "days",
"absolute": false
}
How inputs are converted
Inputs not listed arrive as sent.
| Input | Conversion | What it does |
|---|---|---|
Absolute (absolute) | js-boolean | any truthy value is true, anything else false |
Warnings it shows in the editor
| Warning | Settings it points at | Id |
|---|---|---|
| Unit is not one of the eight | unit | date-difference/unknown-unit |
Version history
- Version 3: A Unit that is not one of the eight now shows as the node's warning in this spec, as the runtime has shown it since v2. Before, the spec left it out. Existing projects are not migrated: only the editor is told, and the runtime already told it. Nothing a wire carries changes.
The full spec
The spec's source, as the docs were generated. Helpers it imports live beside it in packages/nodegx-node-spec/src.
/**
* Date Difference (`net.noodl.DateDifference`) — read from
* `packages/noodl-runtime/src/nodes/std-library/date/datedifference.ts` and `date/datemath.ts` on
* 2026-10-01 (NSP-013).
*
* R3 (a): the runtime wins by default. Every rule below cites the line it was read from.
*
* THE RULE, before the citations: the node recomputes in every setter and ABSTAINS until both
* `From` and `To` have arrived (:134 — anything but `undefined`, `null`, `''`). Then: a side that
* could not be read clears `Difference` and pulses `Invalid Date` (:136-142); otherwise
* `Difference` is `To` − `From` in `Unit` (date-math.ts `differenceBetween` — exact and
* fractional in a fixed unit, whole calendar steps in months and years), made positive when
* `Absolute` is on (:144-145 `Math.abs`), and `Changed` pulses (:147). A `Unit` that is empty or not
* one of the eight counts as days (datemath.ts `unitOrDays`); `Absolute` is truthiness (:118 `!!value`).
*
* An unknown `Unit` also shows the editor a warning until it is fixed (`setDiagnostic`, not on the
* wire). v1 graded it as the runtime then did it: it missed every fixed unit and landed on the
* month path, whose last line read anything but `'months'` as YEARS — NSP-013 §6 row D15, fixed in
* P109 ISL-026 with Date Add's C16.
*/
import { defineNode } from '../spec';
import { DATE_EXAMPLES, differenceBetween, isDateUnit, supplied, toDate, UNIT_VALUES, unknownUnitMessage } from './date-math';
type State = {
from: Date | undefined;
to: Date | undefined;
fromSupplied: boolean;
toSupplied: boolean;
unit: unknown;
absolute: boolean;
difference: number | undefined;
}
/** :132-148 `_recompute`, over the state the setter has just changed (`delta` is what it stored). */
function recompute(s: State, delta: Partial<State>): { says: string; set: Partial<State>; send: Array<'difference'>; emit?: Array<'changed' | 'failure'> } {
const n = { ...s, ...delta };
if (!n.fromSupplied || !n.toSupplied) return { says: 'From or To not given yet: kept for when both are; nothing is sent', set: delta, send: [] }; // :134
if (n.from === undefined || n.to === undefined) return { says: 'either cannot be read as a date: Difference goes empty', set: { ...delta, difference: undefined }, send: ['difference'], emit: ['failure'] }; // :138-142
const value = differenceBetween(n.from, n.to, isDateUnit(n.unit) ? n.unit : 'days'); // :144, `unitOrDays` — v2 (D15)
return { says: 'otherwise Difference is To minus From in Unit (days when not one of the eight), negative when To is earlier unless Absolute is on', set: { ...delta, difference: n.absolute ? Math.abs(value) : value }, send: ['difference'], emit: ['changed'] }; // :145-147
}
export const DateDifference = defineNode({
type: 'net.noodl.DateDifference',
// v2 (P109 ISL-026, P107-D15 ruled "Use days and warn once", with Date Add's C16): a Unit that is not one of
// the eight counts as days. In v1 it counted as YEARS.
version: 3,
changes: [
{
version: 3,
says: 'A Unit that is not one of the eight now shows as the node\'s warning in this spec, as the runtime has shown it since v2. Before, the spec left it out',
row: 'NSP-019 R35 (P107 s102)',
migration: { none: 'Only the editor is told, and the runtime already told it. Nothing a wire carries changes' }
}
],
// the Unit setter (:75, `setDiagnostic('date-difference/unknown-unit', unknownUnitMessage(value))`): the last Unit that arrived, as it arrived
diagnostics: {
'date-difference/unknown-unit': { says: 'Unit is not one of the eight', settings: ['unit'], message: (_i, s) => unknownUnitMessage(s.unit) }
},
source: 'packages/noodl-runtime/src/nodes/std-library/date/datedifference.ts; packages/noodl-runtime/src/nodes/std-library/date/datemath.ts',
needs: ['timezone'],
// :40-43 initialize — unit 'days', absolute false
state: { from: undefined, to: undefined, fromSupplied: false, toSupplied: false, unit: 'days', absolute: false, difference: undefined } as State,
inputs: {
// :45-55
from: { type: 'date', coerce: 'none', examples: DATE_EXAMPLES, displayName: 'From', group: 'General', description: 'The earlier instant, in the reading that makes Difference positive' },
// :56-66
to: { type: 'date', coerce: 'none', examples: DATE_EXAMPLES, displayName: 'To', group: 'General', description: 'The later instant. Difference is negative when this is actually earlier than From' },
// :67-80 — stored raw (:77)
unit: {
type: 'enum',
enums: UNIT_VALUES,
default: 'days',
coerce: 'none',
displayName: 'Unit',
group: 'General',
description:
'What Difference counts. Fixed units are exact and fractional (36 hours is 1.5 days); months ' +
'and years are whole calendar steps',
examples: ['not-a-unit']
},
// :81-91 — `!!value` (:88)
absolute: { type: 'boolean', default: false, coerce: 'js-boolean', displayName: 'Absolute', group: 'General', description: 'Drop the sign, so the output is a distance rather than a direction' }
},
outputs: {
// :94-102
difference: { type: 'number', from: (s) => s.difference, displayName: 'Difference', group: 'Values', description: 'To minus From, counted in Unit. Unset while either date is missing or unreadable' },
// :103-108
changed: { type: 'signal', displayName: 'Changed', group: 'Events', description: 'Fires after Difference has been recomputed' },
// :109-114
failure: { type: 'signal', displayName: 'Invalid Date', group: 'Events', description: 'Fires when a date arrived that could not be read, leaving Difference unset' }
}
}).on({
from: (s, v) => recompute(s, { fromSupplied: supplied(v), from: toDate(v) }), // :50-54
to: (s, v) => recompute(s, { toSupplied: supplied(v), to: toDate(v) }), // :61-65
unit: (s, v) => recompute(s, { unit: v }), // :76-79
absolute: (s, v) => recompute(s, { absolute: v }) // :87-90
});
This page is generated from node-catalog-enriched.json, the node's spec in packages/nodegx-node-spec and the export ledger. Do not edit it by hand — run npm run docs:nodes to regenerate, and fix the source instead.