rate: fix rounding error in tokensFromDuration

tokensFromDuration performs a unit conversion by multiplying the limit
by the duration in seconds.

Make tokensFromDuration perform conversion by unit-wise multiplication
of limit by seconds and nanoseconds, instead of the conversion to
time.(Duration).Seconds that truncated the nanoseconds.

Fixes golang/go#34861

Change-Id: I94d290d3e32a87541d1702f2b58180270e1a9e96
Reviewed-on: https://go-review.googlesource.com/c/time/+/200900
Run-TryBot: Emmanuel Odeke <emm.odeke@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Emmanuel Odeke <emm.odeke@gmail.com>
diff --git a/rate/rate.go b/rate/rate.go
index 85c18b5..3cd1498 100644
--- a/rate/rate.go
+++ b/rate/rate.go
@@ -387,5 +387,9 @@
 // tokensFromDuration is a unit conversion function from a time duration to the number of tokens
 // which could be accumulated during that duration at a rate of limit tokens per second.
 func (limit Limit) tokensFromDuration(d time.Duration) float64 {
-	return d.Seconds() * float64(limit)
+	// Split the integer and fractional parts ourself to minimize rounding errors.
+	// See golang.org/issues/34861.
+	sec := float64(d/time.Second) * float64(limit)
+	nsec := float64(d%time.Second) * float64(limit)
+	return sec + nsec/1e9
 }
diff --git a/rate/rate_test.go b/rate/rate_test.go
index 448300d..b0ed34d 100644
--- a/rate/rate_test.go
+++ b/rate/rate_test.go
@@ -131,6 +131,15 @@
 	})
 }
 
+// Ensure that tokensFromDuration doesn't produce
+// rounding errors by truncating nanoseconds.
+// See golang.org/issues/34861.
+func TestLimiter_noTruncationErrors(t *testing.T) {
+	if !NewLimiter(0.7692307692307693, 1).Allow() {
+		t.Fatal("expected true")
+	}
+}
+
 func TestSimultaneousRequests(t *testing.T) {
 	const (
 		limit       = 1