# Water quality

> Compare water samples with WHO, EU or Brazilian limits, compute ion balance, hydrochemical facies and Piper and Stiff values, and check field QA/QC with @welldot/utils.

`water_samples` is a ledger of samples, each with its field and laboratory results ([water samples](/format/spec/v2/water-quality#water_samples)). The file records only what was measured. Exceedances, ion balance and facies are derived on demand, and drinking water limits are chosen by the reader, never stored ([limit sets](/format/spec/v2/water-quality#limit-sets-are-not-part-of-the-file)).

The outputs below come from two [example wells](/format/reference/examples): `extremoz-rn-barreiras-raso` (nitrate rising in a shallow well) and `quixeramobim-ce-cristalino-salobro` (brackish water in crystalline rock).

## Which samples count

A corrected laboratory report is a new sample whose `corrects` points to the old one, and a result whose `validation.status` is `rejected` is excluded from every derivation ([result rules](/format/spec/v2/water-quality#result-rules)). Start from the effective list:

```ts
import { getEffectiveWaterSamples, isResultUsable } from '@welldot/utils';

const samples = getEffectiveWaterSamples(well); // not retracted, sorted by datetime, sequence, file order
const latest = samples.at(-1)!;
const usable = latest.results.filter(isResultUsable);
```

## Latest value of a parameter

```ts
import { getLatestResult } from '@welldot/utils';

getLatestResult(well, 'nitrate_as_n');
// { sample: { id: 'am-2025', … }, result: { parameter: {…}, value: 12.6, measured_in: 'lab' } }
```

It skips blanks and rejected results, and matches CAS numbers to their `welldot` code.

## Exceedances

Pick a limit set from `@welldot/core` ([Water quality vocabulary](/core/guides/water-quality-vocabulary)) and compare:

```ts
import { BR_GM_MS_888_2021, WHO_GDWQ_2022 } from '@welldot/core';
import { getExceedances } from '@welldot/utils';

getExceedances(latest, BR_GM_MS_888_2021);
// [
//   { result_index: 0, parameter: { code: 'ph', … }, limit: { min: 6, max: 9, … }, kind: 'below_min' },
//   { result_index: 3, parameter: { code: 'nitrate_as_n', … }, limit: { max: 10, … }, kind: 'above_max' }
// ]

getExceedances(latest, WHO_GDWQ_2022); // [] (WHO limits nitrate as NO₃, not as N)
```

Each exceedance has the result's index, the parameter, the limit and its `kind` (`above_max`, `below_min`, or a presence where absence is required). Censored values are handled: `< 5` cannot exceed a limit of 10. Turbidity in FAU is never compared with nephelometric limits. You can also pass your own `Limit[]`.

::note
The same sample can pass one standard and fail another. That is why limits stay out of the file: the reader chooses the standard ([jurisdiction neutrality](/format/spec/v2/overview#design-principles)).
::

## Hydrochemistry

```ts
import {
  getEffectiveWaterSamples,
  getHydrochemicalFacies,
  getIonBalance,
  getPiperCoordinates,
  getStiffValues,
} from '@welldot/utils';

const sample = getEffectiveWaterSamples(brackishWell).at(-1)!;

getIonBalance(sample);
// { cations_meq: 44.29, anions_meq: 44.51, error_pct: -0.26 }

getHydrochemicalFacies(sample); // { cation: 'mixed', anion: 'chloride' }
getStiffValues(sample); // meq/L of Na+K, Ca, Mg, Cl, HCO₃+CO₃, SO₄
getPiperCoordinates(sample); // meq % in each triangle and the diamond point
```

The ion balance error is the usual check on a complete major-ion analysis: within about ±5 % it is considered consistent. Molar masses and charges come from the parameter vocabulary in `@welldot/core`. `getAcidDrainageIndicators(sample)` returns net alkalinity and the sulfate/chloride ratio when the analysis has them.

## Field QA/QC

| Function                                                          | Checks                                                                                                   |
| ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `getRelativePercentDifferences(well, sampleId)`                   | Relative percent difference per parameter between a field duplicate or split and its `parent_sample_id`. |
| `getBlankContamination(well)`                                     | Substances detected in field, trip and equipment blanks, with their campaign.                            |
| `getHoldingTimes(sample)`                                         | Hours from collection to analysis per result, honoring day-resolution dates.                             |
| `getReceivedTemperatureCompliance(sample, maxC?)`                 | Whether the sample arrived at the lab at or below `maxC` (6 °C by default).                              |
| `getPurgeStabilization(purge, criteria?, window?)`                | Whether the purge readings stabilized, per parameter, over the last readings.                            |
| `getSampleDepth(well, sample)` / `isFormationWater(well, sample)` | The sampling depth, and whether it lies in a screened interval below the static level.                   |

```ts
import { getRelativePercentDifferences } from '@welldot/utils';

getRelativePercentDifferences(well, 'am-2025-dup');
// [{ key: 'nitrate_as_n', original: 2.8, duplicate: 2.7, rpd_pct: 3.64 }, …]
```

## Warnings

`getWaterSampleWarnings(well)` lists the water quality conditions the spec marks as warnings, such as a field duplicate without `parent_sample_id`, a sampling depth outside the screens, a result analyzed before it was collected or a duplicated result, with the sample ids and result indexes ([validation warnings](/format/spec/v2/water-quality#validation-warnings-never-rejection)).

## Display

```ts
import { formatWaterQualityResult, parameterUnitSymbol } from '@welldot/utils';

const labels = {
  notDetected: 'n.d.',
  present: 'present',
  absent: 'absent',
  estimated: 'est.',
};

for (const r of latest.results) {
  console.log(formatWaterQualityResult(r, labels), parameterUnitSymbol(r));
  // '< 5' 'mg/L', '0.004 (est.)' 'mg/L', 'absent' '', …
}
```

Labels are passed in, so you keep your own translations.
