npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

chronologic-validator

v0.1.2

Published

Framework-agnostic TypeScript library for validating ordered date field sequences

Readme

chronologic-validator

npm version npm downloads CI License: MIT TypeScript

Framework-agnostic TypeScript library for validating ordered date field sequences — with adapters for React Hook Form and Zod.


The Problem

Any form with multiple date fields that must follow a specific order has the same validation requirement: date A must come before date B, which must come before date C.

Project timelines, hiring pipelines, booking systems, contract periods, event scheduling — the problem is universal. Yet every team solves it differently: ad-hoc comparisons scattered through component logic, awkward Yup refinements, or bespoke hooks that aren't reusable.

chronologic-validator gives you a clean, composable, TypeScript-first solution with a framework-agnostic core and optional adapters for React Hook Form and Zod.


Features

  • Framework-agnostic core — works anywhere JavaScript runs
  • React Hook Form adapter — watches fields and re-validates on change
  • Zod adapter — compose with your existing schemas via superRefine
  • Configurable null handling — skip or surface errors for empty fields
  • Typed error reason codes — distinguish NULL_VALUE from ORDER_VIOLATION
  • Custom error message templates — full control over error text
  • Timezone-safe — all dates normalised to midnight UTC before comparison
  • No cascading errors — a single invalid field doesn't pollute the rest
  • Dual ESM/CJS output — works in all modern JavaScript environments
  • Tree-shakeable — adapters are separate sub-path exports; you only pay for what you use

Installation

npm install chronologic-validator

The core package has zero dependencies. Adapters require their respective peer dependencies:

# For the React Hook Form adapter
npm install react-hook-form

# For the Zod adapter
npm install zod

Quick Start

Core (framework-agnostic)

import { validateDateSequence } from 'chronologic-validator'

const result = validateDateSequence([
  { key: 'startDate',  label: 'Start Date',  value: '2024-01-15' },
  { key: 'reviewDate', label: 'Review Date', value: '2024-03-01' },
  { key: 'endDate',    label: 'End Date',    value: '2024-06-30' },
])

console.log(result.valid)   // true
console.log(result.errors)  // {}

When dates are out of order:

const result = validateDateSequence([
  { key: 'startDate', label: 'Start Date', value: '2024-06-01' },
  { key: 'endDate',   label: 'End Date',   value: '2024-01-01' }, // ❌ before start
])

console.log(result.valid)
// false

console.log(result.errors)
// { endDate: 'End Date must be after Start Date' }

console.log(result.results[1].reason)
// 'ORDER_VIOLATION'

React Hook Form Adapter

import { useForm } from 'react-hook-form'
import { useChronologicalValidation } from 'chronologic-validator/react-hook-form'

function ProjectForm() {
  const { control, register } = useForm({
    defaultValues: {
      startDate:  '',
      reviewDate: '',
      endDate:    '',
    },
  })

  const { errors, isValid } = useChronologicalValidation(control, [
    { key: 'startDate',  label: 'Start Date',  name: 'startDate'  },
    { key: 'reviewDate', label: 'Review Date', name: 'reviewDate' },
    { key: 'endDate',    label: 'End Date',    name: 'endDate'    },
  ])

  return (
    <form>
      <div>
        <label>Start Date</label>
        <input type="date" {...register('startDate')} />
      </div>

      <div>
        <label>Review Date</label>
        <input type="date" {...register('reviewDate')} />
        {errors.reviewDate && <p>{errors.reviewDate}</p>}
      </div>

      <div>
        <label>End Date</label>
        <input type="date" {...register('endDate')} />
        {errors.endDate && <p>{errors.endDate}</p>}
      </div>

      <button type="submit" disabled={!isValid}>
        Submit
      </button>
    </form>
  )
}

Zod Adapter

import { z } from 'zod'
import { chronologicalRefinement } from 'chronologic-validator/zod'

const schema = z
  .object({
    startDate:  z.string().nullable(),
    reviewDate: z.string().nullable(),
    endDate:    z.string().nullable(),
  })
  .superRefine(
    chronologicalRefinement([
      { key: 'startDate',  label: 'Start Date'  },
      { key: 'reviewDate', label: 'Review Date' },
      { key: 'endDate',    label: 'End Date'    },
    ])
  )

const result = schema.safeParse({
  startDate:  '2024-01-15',
  reviewDate: '2024-03-01',
  endDate:    '2024-06-30',
})

console.log(result.success) // true

Or use the convenience wrapper for simpler cases:

import { chronologicalSchema } from 'chronologic-validator/zod'

const schema = chronologicalSchema([
  { key: 'from', label: 'From' },
  { key: 'to',   label: 'To'   },
])

API Reference

validateDateSequence(fields, options?)

The core validation function.

fields: DateField[]

An ordered array of date fields. The array order defines the expected chronological order.

interface DateField<K extends string = string> {
  key: K       // unique identifier — appears in the errors map
  label: string // human-readable name — used in error messages
  value: Date | string | number | null | undefined
}

The value field accepts Date objects, ISO strings, timestamps (numbers), null, and undefined. All values are normalised to midnight UTC before comparison.

options: ValidationOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | allowEqual | boolean | false | When true, adjacent dates may be equal (same day). When false, strict ordering is enforced. | | skipNull | boolean | true | When true, null/undefined fields are silently skipped. When false, they produce an explicit error. | | orderErrorTemplate | string | '{{label}} must be after {{prevLabel}}' | Custom template for ordering errors. Use {{label}} and {{prevLabel}} as placeholders. | | nullErrorTemplate | string | '{{label}} is required and cannot be empty' | Custom template for null errors (only used when skipNull: false). Use {{label}} as a placeholder. |

ValidationResult

interface ValidationResult<K extends string = string> {
  valid: boolean                        // true only if ALL fields passed
  results: FieldValidationResult<K>[]   // per-field results
  errors: Partial<Record<K, string>>    // map of failing keys to error messages
}

Each entry in results is a discriminated union:

type FieldValidationResult<K extends string = string> =
  | { key: K; valid: true;  error: null }
  | { key: K; valid: false; error: string; reason: ValidationErrorReason }

type ValidationErrorReason = 'NULL_VALUE' | 'ORDER_VIOLATION'

The reason field lets you handle different failure types differently in your UI.


Handling Null Fields

By default (skipNull: true), empty fields are silently skipped and the sequence continues from the last non-null value. This is ideal for forms where fields are filled in progressively.

// Phase 2 is empty — validation continues between Phase 1 and Phase 3
validateDateSequence([
  { key: 'phase1', label: 'Phase 1', value: '2024-01-01' },
  { key: 'phase2', label: 'Phase 2', value: null },          // skipped
  { key: 'phase3', label: 'Phase 3', value: '2024-06-01' },
])
// result.valid === true

When skipNull: false, empty fields produce an explicit error with the reason code NULL_VALUE:

validateDateSequence(
  [{ key: 'phase1', label: 'Phase 1', value: null }],
  { skipNull: false }
)
// result.errors.phase1 === 'Phase 1 is required and cannot be empty'
// result.results[0].reason === 'NULL_VALUE'

Custom Error Messages

validateDateSequence(
  [
    { key: 'from', label: 'Contract Start', value: '2024-06-01' },
    { key: 'to',   label: 'Contract End',   value: '2024-01-01' },
  ],
  {
    orderErrorTemplate: '{{label}} must come after {{prevLabel}}',
    nullErrorTemplate:  '{{label}} cannot be left blank',
  }
)
// result.errors.to === 'Contract End must come after Contract Start'

Design Decisions

Timezone-safe date normalisation

All date inputs are normalised to midnight UTC (Date.UTC(year, month, day)) before comparison. This prevents the common off-by-one bugs that occur when comparing dates across different timezones — for example, new Date('2024-03-15') < new Date('2024-03-15T01:00:00') evaluates to true in some timezones, producing a false ordering violation.

No cascading errors

After an invalid field is detected, the comparison cursor still advances to that field's value. This means a single out-of-order date doesn't cause every subsequent field to also fail — only the genuinely invalid field is flagged.

Typed reason codes

The reason field (NULL_VALUE | ORDER_VIOLATION) lets consuming code handle different failure types differently — for example, showing a "required" indicator for null fields versus an ordering error icon for missequenced ones.

Framework-agnostic core with tree-shakeable adapters

The core library has zero runtime dependencies. The React Hook Form and Zod adapters are published as separate sub-path exports (chronologic-validator/react-hook-form, chronologic-validator/zod), so consumers who don't use those frameworks pay zero bundle cost.


React Hook Form Adapter API

useChronologicalValidation(control, fields, options?)

| Parameter | Type | Description | |-----------|------|-------------| | control | Control<TFieldValues> | The RHF control object from useForm | | fields | ChronologicalFieldConfig[] | Field definitions including the RHF field name | | options | UseChronologicalValidationOptions | All ValidationOptions plus validateOnChange |

Returns { result, errors, isValid, validate }.

The hook uses useWatch internally, so validation re-runs automatically whenever any of the watched fields change.


Zod Adapter API

chronologicalRefinement(fields, options?)

Returns a refinement function for use with .superRefine(). Attaches Zod issues directly to the failing field paths, making it fully compatible with zodResolver from @hookform/resolvers.

chronologicalSchema(fields, options?)

Convenience wrapper that creates a complete Zod object schema with all date fields typed as string | null | undefined and chronological validation built in.


Contributing

Contributions, bug reports, and feature requests are welcome. Please open an issue before submitting a PR so we can discuss the change first.

git clone https://github.com/martinschike/chronologic-validator.git
cd chronologic-validator
npm install
npm test

License

MIT © Martins Okafor