Skip to content
go-libfossil Artifact [7a394793b7]
Login

Artifact [7a394793b7]

Artifact 7a394793b7a9cb2d718007b20aafb59376f74d2d9bba67eb2d4d9cf3424d3481:


package fsltype

import (
	"math"
	"runtime"
)

// Julian day numbers the way fossil stores them (#258).
//
// Fossil computes a time's Julian day with SQLite's julianday(), which counts
// whole milliseconds (iJD) and divides: iJD/86400000.0. Where it binds that
// double to a statement (tag_insert, so tagxref.mtime) the exact value is
// stored. Where it formats it into SQL text with its own printf("%.17g") —
// every event, plink, attachment and forumpost time — the text carries 16
// significant digits, produced digit by digit in long double arithmetic, and
// SQLite reads it back with its own, not correctly rounded, conversion
// (sqliteAtoF). JulianDayFromMillis gives the first,
// TextJulianDayFromMillis the second.
//
// The second depends on the platform fossil was built for, because fossil's
// printf (not SQLite: its julianday() counts integer milliseconds and its
// text-to-double conversion is integer arithmetic) carries the value in
// long double. That is a 64-bit double on macOS arm64 and Windows (MSVC),
// the 80-bit x87 format with a 64-bit significand on x86-64 Linux, macOS
// and the BSDs, and IEEE binary128 with a 113-bit significand on arm64
// Linux. The 16th digit, and so the stored double, differs between them for
// about one time in five. TextJulianDayFromMillis computes in the long
// double of the platform go-libfossil runs on (longDoubleBits), so it is
// bit-identical to the fossil built for that same platform: verified against
// fossil 2.28 on macOS arm64 and linux-x64. The binary128 case follows the
// same arithmetic but no fossil built that way has been compared. A
// repository written on one platform and rebuilt by fossil on another can
// therefore differ in these times by a unit or two in the last place --
// as two fossils on those platforms differ from each other -- never by a
// millisecond.

// julianEpochMillis is the Unix epoch as SQLite's iJD: 2440587.5 days in
// milliseconds.
const julianEpochMillis = 210866760000000

const millisPerDay = 86400000.0

// fossilRealDigits is how many significant digits fossil's %.17g emits.
const fossilRealDigits = 16

// JulianDayFromMillis returns SQLite's julianday() for a time given as
// milliseconds since the Unix epoch: the value fossil binds for tagxref.mtime.
func JulianDayFromMillis(millis int64) float64 {
	return float64(millis+julianEpochMillis) / millisPerDay
}

// MillisFromJulianDay returns the time a Julian day names, as milliseconds
// since the Unix epoch, converted exactly as SQLite's date.c does:
// (sqlite3_int64)(r*86400000.0+0.5), less the epoch. Every Julian day is
// positive, so the cast truncates where floor would.
func MillisFromJulianDay(jd float64) int64 {
	return int64(jd*millisPerDay+0.5) - julianEpochMillis
}

// TextJulianDayFromMillis returns the Julian day fossil stores where it writes
// one as SQL text: event, plink, attachment and forumpost times.
func TextJulianDayFromMillis(millis int64) float64 {
	return textJulianDay(millis, longDoubleBits)
}

// textJulianDay is TextJulianDayFromMillis for a fossil whose long double has
// a significand of prec bits.
func textJulianDay(millis int64, prec uint) float64 {
	return sqliteAtoF(fossilPrintf17g(JulianDayFromMillis(millis), prec))
}

// longDoubleBits is the significand width of the C long double fossil uses
// on the platform this program runs on: see the package comment above.
// Platforms it does not name fall back to 53 bits, which is wrong for some:
// ppc64le's long double is IBM double-double and mips64's is binary128, so
// fossil's last digit there may differ from ours.
var longDoubleBits = platformLongDoubleBits(runtime.GOOS, runtime.GOARCH)

func platformLongDoubleBits(goos, goarch string) uint {
	switch {
	case goos == "windows":
		return 53
	case goarch == "amd64" || goarch == "386":
		return 64
	case goos == "darwin" || goos == "ios":
		return 53
	case goarch == "arm64" || goarch == "riscv64" || goarch == "s390x" || goarch == "loong64":
		return 113
	default:
		return 53
	}
}

// fossilPrintf17g is fossil's printf("%.17g", v) (src/printf.c, etGENERIC)
// for a positive v, computed in a long double with a prec-bit significand
// and round-to-nearest-even, as the C arithmetic is. It normalizes v to
// [1, 10) by repeated scaling, adds a rounder (a double, as fossil's is) at
// the 16th digit, and emits digits by truncate-and-multiply; past 16
// significant digits it emits '0'. A 53-bit long double is a double, so that
// case is computed in float64 (printf17gDouble); the wider ones need
// extended precision (printf17gLong), which a wasm build, whose long double
// is a double, leaves out.
func fossilPrintf17g(v0 float64, prec uint) string {
	if !(v0 > 0) || math.IsInf(v0, 0) {
		panic("fsltype.fossilPrintf17g: only finite positive values")
	}
	if prec != 53 && prec != 64 && prec != 113 {
		panic("fsltype.fossilPrintf17g: unknown long double precision")
	}
	if prec == 53 {
		return printf17gDouble(v0)
	}
	return printf17gLong(v0, prec)
}

// printf17gPrecision is %.17g's precision as etGENERIC counts it, less the
// leading digit.
const printf17gPrecision = 17 - 1

// printf17gRounder is the double fossil adds at the 16th significant digit:
// 0.5 scaled by 0.1 printf17gPrecision times, in double arithmetic.
func printf17gRounder() float64 {
	rounder := 0.5
	for i := 0; i < printf17gPrecision; i++ {
		rounder *= 0.1
	}
	return rounder
}

// printf17gDouble is fossilPrintf17g in double arithmetic. Every product is
// converted explicitly, so no step fuses into a multiply-add and each rounds
// to a double, as the C does.
func printf17gDouble(v float64) string {
	const precision = printf17gPrecision
	exp := 0
	for v >= 1e32 && exp <= 350 {
		v = float64(v * 1e-32)
		exp += 32
	}
	for v >= 1e8 && exp <= 350 {
		v = float64(v * 1e-8)
		exp += 8
	}
	for v >= 10 && exp <= 350 {
		v = float64(v * 0.1)
		exp++
	}
	for v < 1e-8 && exp >= -350 {
		v = float64(v * 1e8)
		exp -= 8
	}
	for v < 1 && exp >= -350 {
		v = float64(v * 10)
		exp--
	}
	v = float64(v + printf17gRounder())
	if v >= 10 {
		v = float64(v * 0.1)
		exp++
	}
	if exp < -4 || exp > precision {
		panic("fsltype.printf17gDouble: exponent form is not used for Julian days")
	}
	return fossilDigits(exp, precision-exp, func() int64 {
		d := int64(v)
		v = float64(v - float64(d))
		v = float64(v * 10)
		return d
	})
}

// fossilDigits emits fossil's fixed-point digits for a value normalized to
// [1, 10): exp+1 before the point, fraction after it, trailing zeros removed.
// next is et_getdigit: it returns the value's integer part and leaves the
// value as its fraction times ten, in the value's precision.
func fossilDigits(exp, fraction int, next func() int64) string {
	if exp < 0 {
		panic("fsltype.fossilDigits: leading zero form is not used for Julian days")
	}
	buf := make([]byte, 0, exp+fraction+2)
	emitted := 0
	digit := func() byte {
		emitted++
		if emitted > fossilRealDigits {
			return '0'
		}
		d := next()
		if d < 0 || d > 9 {
			panic("fsltype.fossilDigits: digit out of range")
		}
		return byte('0' + d)
	}
	for i := 0; i <= exp; i++ {
		buf = append(buf, digit())
	}
	buf = append(buf, '.')
	for i := 0; i < fraction; i++ {
		buf = append(buf, digit())
	}
	for buf[len(buf)-1] == '0' {
		buf = buf[:len(buf)-1]
	}
	if buf[len(buf)-1] == '.' {
		buf = buf[:len(buf)-1]
	}
	return string(buf)
}