all repos

clerk @ 731ba2401512c581ecee570bdbcc5bef6905a4df

missing tooling for ledger/hledger
6 files changed, 106 insertions(+), 0 deletions(-)
linter: add chronological order
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-06-27 12:41:04 +0300
Authored at: 2026-06-26 15:44:13 +0300
Change ID: kvywvuqrzylyryumvmoswknlomlzyqqy
Parent: 1b934a8
M internal/linter/linter_test.go
···
        17
        17
         	"missing-status":           {&MissingStatus{}},

      
        18
        18
         	"account-depth":            {&AccountDepthLimit{MaxDepth: 3}},

      
        19
        19
         	"multiple-omitted-amounts": {&MultipleOmittedAmounts{}},

      
        
        20
        +	"orderdate":                {&OrderDate{}},

      
        20
        21
         }

      
        21
        22
         

      
        22
        23
         func TestLinter(t *testing.T) {

      
A internal/linter/rule_orderdate.go
···
        
        1
        +package linter

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +

      
        
        6
        +	"olexsmir.xyz/clerk/journal/ast"

      
        
        7
        +)

      
        
        8
        +

      
        
        9
        +// OrderDate checks that transactions are in chronological order by date.

      
        
        10
        +type OrderDate struct{}

      
        
        11
        +

      
        
        12
        +func (OrderDate) ID() RuleID         { return "orderdate" }

      
        
        13
        +func (OrderDate) Severity() Severity { return SeverityWarning }

      
        
        14
        +func (r *OrderDate) CheckJournal(j *ast.Journal) []Find {

      
        
        15
        +	var finds []Find

      
        
        16
        +	var anchor *ast.Date

      
        
        17
        +	for _, entry := range j.Entries {

      
        
        18
        +		txn, ok := entry.(*ast.Transaction)

      
        
        19
        +		if !ok {

      
        
        20
        +			continue

      
        
        21
        +		}

      
        
        22
        +

      
        
        23
        +		d := txn.Date

      
        
        24
        +		if anchor != nil {

      
        
        25
        +			cmp := compareDate(d, *anchor)

      
        
        26
        +			if cmp < 0 {

      
        
        27
        +				finds = append(finds, Find{

      
        
        28
        +					Code:     r.ID(),

      
        
        29
        +					Severity: r.Severity(),

      
        
        30
        +					Message: fmt.Sprintf("transaction is out of chronological order (date %s before %s)",

      
        
        31
        +						dateString(d), dateString(*anchor)),

      
        
        32
        +					Span: d.Span,

      
        
        33
        +				})

      
        
        34
        +				continue

      
        
        35
        +			}

      
        
        36
        +		}

      
        
        37
        +

      
        
        38
        +		anchor = &d

      
        
        39
        +	}

      
        
        40
        +

      
        
        41
        +	return finds

      
        
        42
        +}

      
        
        43
        +

      
        
        44
        +// compareDate returns -1 if a < b, 0 if equal, 1 if a > b.

      
        
        45
        +func compareDate(a, b ast.Date) int {

      
        
        46
        +	if a.Year != b.Year {

      
        
        47
        +		if a.Year < b.Year {

      
        
        48
        +			return -1

      
        
        49
        +		}

      
        
        50
        +		return 1

      
        
        51
        +	}

      
        
        52
        +	if a.Month != b.Month {

      
        
        53
        +		if a.Month < b.Month {

      
        
        54
        +			return -1

      
        
        55
        +		}

      
        
        56
        +		return 1

      
        
        57
        +	}

      
        
        58
        +	if a.Day != b.Day {

      
        
        59
        +		if a.Day < b.Day {

      
        
        60
        +			return -1

      
        
        61
        +		}

      
        
        62
        +		return 1

      
        
        63
        +	}

      
        
        64
        +	return 0

      
        
        65
        +}

      
        
        66
        +

      
        
        67
        +func dateString(d ast.Date) string {

      
        
        68
        +	return fmt.Sprintf("%d-%02d-%02d", d.Year, d.Month, d.Day)

      
        
        69
        +}

      
M internal/linter/rules.go
···
        31
        31
         	&MissingStatus{},

      
        32
        32
         	&AccountDepthLimit{MaxDepth: 4},

      
        33
        33
         	&MultipleOmittedAmounts{},

      
        
        34
        +	&OrderDate{},

      
        34
        35
         }

      
M internal/linter/testdata/correct.input
···
        1
        1
         2024-05-01 * "groceries"

      
        2
        2
             expenses:food   50.00 USD

      
        3
        3
             assets:cash

      
        
        4
        +

      
        
        5
        +2024-05-02 * "groceries"

      
        
        6
        +    expenses:food   40.00 USD

      
        
        7
        +    assets:cash

      
A internal/linter/testdata/orderdate.golden
···
        
        1
        +orderdate.journal:5:1: orderdate: transaction is out of chronological order (date 2026-01-10 before 2026-01-15)

      
        
        2
        +orderdate.journal:9:1: orderdate: transaction is out of chronological order (date 2026-01-12 before 2026-01-15)

      
        
        3
        +orderdate.journal:17:1: orderdate: transaction is out of chronological order (date 2026-01-20 before 2026-02-01)

      
        
        4
        +orderdate.journal:25:1: orderdate: transaction is out of chronological order (date 2025-12-01 before 2026-03-01)

      
A internal/linter/testdata/orderdate.input
···
        
        1
        +2026-01-15 * "Transaction 1"

      
        
        2
        +  Expenses:Food      $10

      
        
        3
        +  Assets:Cash

      
        
        4
        +

      
        
        5
        +2026-01-10 * "Transaction 2 - out of order (before Txn 1)"

      
        
        6
        +  Expenses:Food      $20

      
        
        7
        +  Assets:Cash

      
        
        8
        +

      
        
        9
        +2026-01-12 * "Transaction 3 - would slip through old prev logic (12 < 15 but 12 > 10)"

      
        
        10
        +  Expenses:Food      $8

      
        
        11
        +  Assets:Cash

      
        
        12
        +

      
        
        13
        +2026-02-01 * "Transaction 4"

      
        
        14
        +  Expenses:Food      $15

      
        
        15
        +  Assets:Cash

      
        
        16
        +

      
        
        17
        +2026-01-20 * "Transaction 5 - out of order (before Txn 4)"

      
        
        18
        +  Expenses:Food      $5

      
        
        19
        +  Assets:Cash

      
        
        20
        +

      
        
        21
        +2026-03-01 * "Transaction 6"

      
        
        22
        +  Expenses:Food      $25

      
        
        23
        +  Assets:Cash

      
        
        24
        +

      
        
        25
        +2025-12-01 * "Transaction 7 - entirely before everything"

      
        
        26
        +  Expenses:Food      $30

      
        
        27
        +  Assets:Cash