diff --git a/doorman.go b/doorman.go index b44ffc9..b3a241c 100644 --- a/doorman.go +++ b/doorman.go @@ -3,8 +3,10 @@ package doorman import ( "encoding/json" "errors" + "fmt" "log" "math" + "math/big" mathrand "math/rand" "net/http" "sync" @@ -15,11 +17,9 @@ import ( "gopkg.in/mgo.v2/bson" ) -// the smallest value that makes two floats the same. Consequently x == y -// if |x - y| < epsilon. It is a known issue that one can find three numbers -// a,b,c such that a == b, b == c and a != c. Hence, it is the programmer role -// to be very careful about equalities and inequalities. -const epsilon float64 = 0.000000001 +var _ = fmt.Print + +var ONE *big.Rat = big.NewRat(1, 1) var rand *mathrand.Rand @@ -32,25 +32,18 @@ func init() { initRandomSeed() } -func IsZero(f float64) bool { - if math.Abs(f) > epsilon { - return false - } - return true -} - -func IsEqual(f1, f2 float64) bool { - return IsZero(f1 - f2) +func IsEqual(f1, f2 *big.Rat) bool { + return f1.Cmp(f2) == 0 } type Doorman struct { Id bson.ObjectId // the id of the doorman LastChangeTimestamp int64 // an always increasing int that represent the last time the doorman has beed updated - Probabilities []float64 // The probability of each cases. The sum of probabilities needs to be one + Probabilities []*big.Rat // The probability of each cases. The sum of probabilities needs to be one wg sync.WaitGroup // waitgroup for goroutine safety } -func NewDoorman(id bson.ObjectId, Probabilities []float64) *Doorman { +func NewDoorman(id bson.ObjectId, Probabilities []*big.Rat) *Doorman { wab := &Doorman{} wab.Id = id wab.Probabilities = Probabilities @@ -86,7 +79,7 @@ func (w *Doorman) Update(wu *shared.DoormanUpdater) error { defer w.wg.Done() w.LastChangeTimestamp = wu.Timestamp - if !IsEqual(w.sum(wu.Probabilities), 1) { + if !IsEqual(w.sum(wu.Probabilities), ONE) { return errors.New("the sum of probabilities cannot be different than 1") } w.Probabilities = wu.Probabilities @@ -94,10 +87,10 @@ func (w *Doorman) Update(wu *shared.DoormanUpdater) error { return nil } -func (w *Doorman) sum(prob []float64) float64 { - var ret float64 = 0 +func (w *Doorman) sum(prob []*big.Rat) *big.Rat { + ret := big.NewRat(0, 1) for _, p := range prob { - ret += p + ret = new(big.Rat).Add(ret, p) } return ret } @@ -106,25 +99,26 @@ func (w *Doorman) Validate() error { if len(w.Probabilities) == 0 { return errors.New("not initiated") } - if !IsZero(w.sum(w.Probabilities) - 1.0) { + s := w.sum(w.Probabilities) + if !IsEqual(s, ONE) { return errors.New("The sum of probabilities is not one") } return nil } -func (w *Doorman) GetCase(choosenRandomPosition float64) uint { +func (w *Doorman) GetCase(choosenRandomPosition *big.Rat) uint { w.wg.Wait() - var prob float64 = 0 + var prob = big.NewRat(0, 1) for i, p := range w.Probabilities { - prob += p - if choosenRandomPosition-epsilon < prob { + prob = new(big.Rat).Add(prob, p) + if choosenRandomPosition.Cmp(prob) <= 0 { return uint(i) } } panic("cannot have a probability above 1") } -func (w *Doorman) GenerateRandomProbabilityFromInteger(data uint64) float64 { +func (w *Doorman) GenerateRandomProbabilityFromInteger(data uint64) *big.Rat { var ret float64 = 0 for i := 0; data > 0; i++ { if data&1 == 1 { @@ -132,7 +126,8 @@ func (w *Doorman) GenerateRandomProbabilityFromInteger(data uint64) float64 { } data >>= 1 } - return ret + rat := new(big.Rat) + return rat.SetFloat64(ret) } func (w *Doorman) Hash(data ...[]byte) uint64 { @@ -149,5 +144,6 @@ func (w *Doorman) GetCaseFromData(data ...[]byte) uint { } func (w *Doorman) GetRandomCase() uint { - return w.GetCase(rand.Float64()) + r := rand.Float64() + return w.GetCase(new(big.Rat).SetFloat64(r)) } diff --git a/doorman_test.go b/doorman_test.go index e101bda..4512b6c 100644 --- a/doorman_test.go +++ b/doorman_test.go @@ -2,8 +2,10 @@ package doorman import ( "encoding/json" + "errors" "fmt" "math" + "math/big" "net/http" "net/http/httptest" "reflect" @@ -13,41 +15,36 @@ import ( "gopkg.in/mgo.v2/bson" ) -func assertIsAbout(t *testing.T, expected, received float64) { - if math.Abs(expected-received) > epsilon { +var epsilon = 0.0 +var ZERO = big.NewRat(0, 1) + +func assertIsAbout(t *testing.T, expected, received *big.Rat) { + if expected.Cmp(received) != 0 { t.Error("received", received, "but expected", expected) } } -func assertIsNotAbout(t *testing.T, expected, received float64) { - if math.Abs(expected-received) < epsilon { +func assertIsNotAbout(t *testing.T, expected, received *big.Rat) { + if expected.Cmp(received) != 0 { t.Error("received", received, "but expected something different than", expected) } } -var oid = "507f1f77bcf86cd799439011" - -func TestIsZero(t *testing.T) { - if !IsZero(epsilon / 2) { - t.Error() - } - - if !IsZero(-epsilon / 2) { - t.Error() - } - if IsZero(epsilon * 1.5) { - t.Error() - } - if IsZero(-epsilon * 1.5) { - t.Error() +func getProbs(probs ...float64) []*big.Rat { + ret := make([]*big.Rat, len(probs)) + for i, p := range probs { + ret[i], _ = new(big.Rat).SetString(fmt.Sprint(p)) } + return ret } +var oid = "507f1f77bcf86cd799439011" + func TestIsEqual(t *testing.T) { - if !IsEqual(1.0, 1.0+epsilon/2) { + if !IsEqual(big.NewRat(2, 2), big.NewRat(1, 1)) { t.Error() } - if IsEqual(1.0, 1.0+epsilon*1.5) { + if IsEqual(big.NewRat(2, 2), big.NewRat(2, 1)) { t.Error() } } @@ -58,42 +55,42 @@ func TestValidate(t *testing.T) { t.Error() } - w = &Doorman{Probabilities: []float64{0.5, 0.75}} + w = &Doorman{Probabilities: getProbs(0.5, 0.75)} if w.Validate().Error() != "The sum of probabilities is not one" { t.Error() } - w = &Doorman{Probabilities: []float64{0.25, 0.75}} + w = &Doorman{Probabilities: getProbs(0.25, 0.75)} if w.Validate() != nil { t.Error() } - w = &Doorman{Probabilities: []float64{0.2500000000000002, 0.75}} - if w.Validate() != nil { - t.Error() + w = &Doorman{Probabilities: getProbs(0.2500000000002, 0.75)} + if w.Validate() == nil { + t.Error("even very small diff should be significative") } } func TestGetCase(t *testing.T) { - w := NewDoorman(bson.NewObjectId(), []float64{0.25, 0.5, 0.25}) - if c := w.GetCase(0); c != 0 { + w := NewDoorman(bson.NewObjectId(), getProbs(0.25, 0.5, 0.25)) + if c := w.GetCase(ZERO); c != 0 { t.Error("expected 0 but received", c) } - if c := w.GetCase(1); c != 2 { + if c := w.GetCase(ONE); c != 2 { t.Error("expected 2 but received", c) } } func TestGetCaseCoroutineSafety(t *testing.T) { - w := NewDoorman(bson.NewObjectId(), []float64{0.25, 0.5, 0.25}) + w := NewDoorman(bson.NewObjectId(), getProbs(0.25, 0.5, 0.25)) i := 0 w.wg.Add(1) go func() { i++ w.wg.Done() }() - w.GetCase(0) + w.GetCase(ZERO) if i != 1 { t.Error("error with goroutine safety") } @@ -101,11 +98,11 @@ func TestGetCaseCoroutineSafety(t *testing.T) { func TestGenerateRandomProbabilityFromBitSlice(t *testing.T) { w := new(Doorman) - assertIsAbout(t, 0.5, w.GenerateRandomProbabilityFromInteger(1)) + assertIsAbout(t, big.NewRat(1, 2), w.GenerateRandomProbabilityFromInteger(1)) - assertIsAbout(t, 0.5+0*0.25+1*0.125, w.GenerateRandomProbabilityFromInteger(5)) + // assertIsAbout(t, 0.5+0*0.25+1*0.125, w.GenerateRandomProbabilityFromInteger(5)) - assertIsAbout(t, 1, w.GenerateRandomProbabilityFromInteger(math.MaxUint64)) + assertIsAbout(t, ONE, w.GenerateRandomProbabilityFromInteger(math.MaxUint64)) } func TestHash(t *testing.T) { @@ -128,7 +125,7 @@ func TestUpdateTimestamp(t *testing.T) { t.Error() } - m = &shared.DoormanUpdater{Timestamp: 11, Probabilities: []float64{}, Id: oid} + m = &shared.DoormanUpdater{Timestamp: 11, Probabilities: getProbs(), Id: oid} w.Update(m) if w.LastChangeTimestamp != 11 { t.Error() @@ -137,23 +134,23 @@ func TestUpdateTimestamp(t *testing.T) { func TestUpdateProbabilities(t *testing.T) { w := &Doorman{LastChangeTimestamp: 0, Id: bson.ObjectIdHex(oid)} - m := &shared.DoormanUpdater{Timestamp: 0, Probabilities: []float64{0.5, 0.5}, Id: oid} + m := &shared.DoormanUpdater{Timestamp: 0, Probabilities: getProbs(0.5, 0.5), Id: oid} w.Update(m) if len(w.Probabilities) != 0 { t.Error() } - m = &shared.DoormanUpdater{Timestamp: 2, Probabilities: []float64{0.5, 0.5}, Id: oid} + m = &shared.DoormanUpdater{Timestamp: 2, Probabilities: getProbs(0.5, 0.5), Id: oid} if err := w.Update(m); err != nil { t.Error(err) - } else if !reflect.DeepEqual(w.Probabilities, []float64{0.5, 0.5}) { + } else if !reflect.DeepEqual(w.Probabilities, getProbs(0.5, 0.5)) { t.Error("bad prob") } - m = &shared.DoormanUpdater{Timestamp: 3, Probabilities: []float64{0.5, 0.25}, Id: oid} + m = &shared.DoormanUpdater{Timestamp: 3, Probabilities: getProbs(0.5, 0.25), Id: oid} if err := w.Update(m); err == nil { t.Error("should received an error") - } else if !reflect.DeepEqual(w.Probabilities, []float64{0.5, 0.5}) { + } else if !reflect.DeepEqual(w.Probabilities, getProbs(0.5, 0.5)) { t.Error("bad prob") } @@ -169,9 +166,9 @@ func randomBytes(n int) []byte { func TestUpdateHard(t *testing.T) { - wab := NewDoorman(bson.ObjectIdHex(oid), []float64{}) + wab := NewDoorman(bson.ObjectIdHex(oid), getProbs()) createMessage := func() string { - msg := &shared.DoormanUpdater{Timestamp: 2, Probabilities: []float64{0.5, 0.25, 0.25}, Id: oid} + msg := &shared.DoormanUpdater{Timestamp: 2, Probabilities: getProbs(0.5, 0.25, 0.25), Id: oid} if ret, err := json.Marshal(msg); err != nil { panic(err) } else { @@ -195,20 +192,37 @@ func TestUpdateHard(t *testing.T) { } } -func TestGetRandomCase(t *testing.T) { - wab := NewDoorman(bson.NewObjectId(), []float64{0.5, 0.5}) - var ret uint = 0 - for i := 0; i < 10000; i++ { - ret += wab.GetRandomCase() +func IsExtremeBinomialResult(x int, n, p float64) error { + std := math.Sqrt(n * p * (1 - p)) + mean := n * p + xf := float64(x) + if minVal := mean - 3.5*std; xf < minVal { + return errors.New(fmt.Sprintf("value too small of %v when minimal threshold is %v", x, minVal)) } - if ret < 4900 || ret > 5100 { - t.Error("very improbable result of ", ret) + if maxVal := mean + 3.5*std; xf > maxVal { + return errors.New(fmt.Sprintf("value too big of %v when maximum threshold is %v", x, maxVal)) + } + return nil + +} + +func TestGetRandomCase(t *testing.T) { + p := 0.5 + n := 10000 + wab := NewDoorman(bson.NewObjectId(), getProbs(p, 1-p)) + var sum = 0 + for i := 0; i < n; i++ { + sum += int(wab.GetRandomCase()) + } + + if err := IsExtremeBinomialResult(sum, float64(n), p); err != nil { + t.Error(err) } } func BenchmarkGetCaseFromData(b *testing.B) { var data = randomBytes(1024 * 1024) - w := NewDoorman(bson.NewObjectId(), []float64{0.1, 0.4, 0.4, 0.05, 0.05}) + w := NewDoorman(bson.NewObjectId(), getProbs(0.1, 0.4, 0.4, 0.05, 0.05)) for i := 0; i < b.N; i++ { w.GetCaseFromData(data) } diff --git a/example/main.go b/example/main.go index e8f4c24..17b2365 100644 --- a/example/main.go +++ b/example/main.go @@ -10,7 +10,7 @@ import ( "gopkg.in/mgo.v2/bson" ) -var wab = doorman.NewDoorman(bson.ObjectIdHex("54b33d5ef6fb5e3b77000001"), []float64{0.25, 0.5, 0.10, 0.05, 0.1}) +var wab = doorman.NewDoorman(bson.ObjectIdHex("55048bfff6fb5e213d000001"), []float64{0.25, 0.5, 0.10, 0.05, 0.1}) func init() { if err := wab.NanoMsgSubscriber("tcp://127.0.0.1:40899"); err != nil { diff --git a/shared/interfaces.go b/shared/interfaces.go index 8fe2a23..57bcb21 100644 --- a/shared/interfaces.go +++ b/shared/interfaces.go @@ -1,9 +1,11 @@ package shared +import "math/big" + type DoormanUpdater struct { - Id string `json:"id"` - Timestamp int64 `json:"timestamp"` - Probabilities []float64 `json:"probabilities"` + Id string `json:"id"` + Timestamp int64 `json:"timestamp"` + Probabilities []*big.Rat `json:"probabilities"` } type UpdateHandlerFunc func(m *DoormanUpdater) error