[config_parser] Use value range size as num_index_points when small

When a report has a small range of valid integer values
(determined by some combination of the report's min_value,
max_value, and max_count fields, depending on the report
type), the precomputed discretization parameter may be
unnecessarily large. After this change, the config parser
sets the effective number of index points for a report
equal to the max of the report's integer range size and
the precomputed number of index points.

Bug: 69287
Change-Id: I79f2d57167cad815bb0ea2b2a8c1996e830d02c9
Reviewed-on: https://fuchsia-review.googlesource.com/c/cobalt/+/485978
Commit-Queue: Laura Peskin <pesk@google.com>
Reviewed-by: Jared Weinstein <jaredweinstein@google.com>
diff --git a/src/bin/config_parser/src/config_parser/populate_privacy_params_test.go b/src/bin/config_parser/src/config_parser/populate_privacy_params_test.go
index 385dd04..d1b7fc1 100644
--- a/src/bin/config_parser/src/config_parser/populate_privacy_params_test.go
+++ b/src/bin/config_parser/src/config_parser/populate_privacy_params_test.go
@@ -27,9 +27,15 @@
 	{"10.0", "20000", "10", "0.0004855785518884659", "15"},
 }
 
+// Test cases where the report's integer range size is larger than the number of index
+// points given by |testParamRecords|.
 func TestPopulateParamsForReport(t *testing.T) {
 	var eventCodeBufferMax uint64 = 10
 
+	var minValue int64 = 0
+	var maxValue int64 = 100
+	var maxCount uint64 = 100
+
 	occurrenceMetric := config.MetricDefinition{
 		MetricName:         "OccurrenceMetric",
 		MetricType:         config.MetricDefinition_OCCURRENCE,
@@ -43,16 +49,22 @@
 		ReportName:   "HighPrivacyFleetwideOccurrenceCounts",
 		ReportType:   config.ReportDefinition_FLEETWIDE_OCCURRENCE_COUNTS,
 		PrivacyLevel: config.ReportDefinition_HIGH_PRIVACY,
+		MinValue:     minValue,
+		MaxValue:     maxValue,
 	}
 	mediumPrivacyFleetwideOccurrenceCountsReport := config.ReportDefinition{
 		ReportName:   "MediumPrivacyFleetwideOccurrenceCounts",
 		ReportType:   config.ReportDefinition_FLEETWIDE_OCCURRENCE_COUNTS,
 		PrivacyLevel: config.ReportDefinition_MEDIUM_PRIVACY,
+		MinValue:     minValue,
+		MaxValue:     maxValue,
 	}
 	lowPrivacyFleetwideOccurrenceCountsReport := config.ReportDefinition{
 		ReportName:   "LowPrivacyFleetwideOccurrenceCounts",
 		ReportType:   config.ReportDefinition_FLEETWIDE_OCCURRENCE_COUNTS,
 		PrivacyLevel: config.ReportDefinition_LOW_PRIVACY,
+		MinValue:     minValue,
+		MaxValue:     maxValue,
 	}
 	noAddedPrivacyFleetwideOccurrenceCountsReport := config.ReportDefinition{
 		ReportName:   "NoAddedPrivacyFleetwideOccurrenceCounts",
@@ -63,6 +75,7 @@
 		ReportName:   "HighPrivacyStringCounts",
 		ReportType:   config.ReportDefinition_STRING_COUNTS,
 		PrivacyLevel: config.ReportDefinition_HIGH_PRIVACY,
+		MaxCount:     maxCount,
 	}
 	noAddedPrivacyStringCountsReport := config.ReportDefinition{
 		ReportName:   "NoAddedPrivacyStringCounts",
@@ -134,6 +147,95 @@
 	}
 }
 
+// Test cases where the report's integer range size is smaller than the number of index
+// points given by |testParamRecords|. In this case the integer range size should be used
+// as the number of index points.
+func TestPopulateParamsForSmallRangeReport(t *testing.T) {
+	var eventCodeBufferMax uint64 = 5
+	var minValue int64 = 0
+	var maxValue int64 = 1
+	var maxCount uint64 = 1
+	var numLinearBuckets uint32 = 2
+
+	linearBuckets := config.LinearIntegerBuckets{NumBuckets: numLinearBuckets}
+	buckets := config.IntegerBuckets{
+		Buckets: &config.IntegerBuckets_Linear{&linearBuckets},
+	}
+
+	occurrenceMetric := config.MetricDefinition{
+		MetricName:         "OccurrenceMetric",
+		MetricType:         config.MetricDefinition_OCCURRENCE,
+		EventCodeBufferMax: eventCodeBufferMax,
+	}
+	integerMetric := config.MetricDefinition{
+		MetricName:         "IntegerMetric",
+		MetricType:         config.MetricDefinition_INTEGER,
+		EventCodeBufferMax: eventCodeBufferMax,
+	}
+	highPrivacyFleetwideOccurrenceCountsReport := config.ReportDefinition{
+		ReportName:   "HighPrivacyFleetwideOccurrenceCounts",
+		ReportType:   config.ReportDefinition_FLEETWIDE_OCCURRENCE_COUNTS,
+		PrivacyLevel: config.ReportDefinition_HIGH_PRIVACY,
+		MinValue:     minValue,
+		MaxValue:     maxValue,
+	}
+	highPrivacyFleetwideHistogramsReport := config.ReportDefinition{
+		ReportName:   "HighPrivacyFleetwideHistograms",
+		ReportType:   config.ReportDefinition_FLEETWIDE_HISTOGRAMS,
+		IntBuckets:   &buckets,
+		PrivacyLevel: config.ReportDefinition_HIGH_PRIVACY,
+		MaxCount:     maxCount,
+	}
+
+	calc, err := privacy.NewPrivacyEncodingParamsCalculatorForTesting(testParamRecords)
+	if err != nil {
+		t.Errorf("Failed precondition: failed to create PrivacyEncodingParamsCalculator. Error: %v", err)
+	}
+
+	type args struct {
+		metric *config.MetricDefinition
+		report *config.ReportDefinition
+	}
+	type expectedParams struct {
+		probBitFlip    float64
+		numIndexPoints uint32
+	}
+	var tests = []struct {
+		input    args
+		valid    bool
+		expected expectedParams
+	}{
+		// Valid inputs:
+		// For each report, the target population is 15000.
+		//
+		// The sparsity is |eventCodeBufferMax| = 5.
+		// The best-match record has key (1.0, 10000, 10).
+		{args{&occurrenceMetric, &highPrivacyFleetwideOccurrenceCountsReport}, true,
+			expectedParams{0.019537480548024178, uint32(maxValue)}},
+		// The sparsity is |eventCodeBufferMax| * |numLinearBuckets| = 10.
+		// The best-match record has key (1.0, 10000, 10).
+		{args{&integerMetric, &highPrivacyFleetwideHistogramsReport}, true,
+			expectedParams{0.019537480548024178, uint32(maxCount)}},
+	}
+	for _, test := range tests {
+		err := populateParamsForReport(calc, test.input.metric, test.input.report)
+		if test.valid && err != nil {
+			t.Errorf("populateParamsForReport() failed for report %s: %v", test.input.report.ReportName, err)
+		} else if !test.valid && err == nil {
+			t.Errorf("populateParamsForReport() accepted invalid input: metric %s, report %s", test.input.metric.MetricName, test.input.report.ReportName)
+		} else {
+			if test.input.report.ProbBitFlip != test.expected.probBitFlip {
+				t.Errorf("populateParamsForReport() wrote incorrect ProbBitFlip for report %s: expected %f, got %f",
+					test.input.report.ReportName, test.expected.probBitFlip, test.input.report.ProbBitFlip)
+			}
+			if test.input.report.NumIndexPoints != test.expected.numIndexPoints {
+				t.Errorf("populateParamsForReport() wrote incorrect NumIndexPoints for report %s: expected %d, got %d",
+					test.input.report.ReportName, test.expected.numIndexPoints, test.input.report.NumIndexPoints)
+			}
+		}
+	}
+}
+
 func TestPopulateParamsForConfig(t *testing.T) {
 	cobalt10Report := config.ReportDefinition{
 		ReportName: "Cobalt1.0Report",
@@ -148,6 +250,8 @@
 		ReportName:   "Cobalt1.1Report",
 		ReportType:   config.ReportDefinition_FLEETWIDE_OCCURRENCE_COUNTS,
 		PrivacyLevel: config.ReportDefinition_HIGH_PRIVACY,
+		MinValue:     0,
+		MaxValue:     10,
 	}
 	cobalt11Metric := config.MetricDefinition{
 		MetricName:         "Cobalt1.1Metric",
diff --git a/src/bin/config_parser/src/privacy/privacy_encoding_params.go b/src/bin/config_parser/src/privacy/privacy_encoding_params.go
index 95c4e3d..df864c1 100644
--- a/src/bin/config_parser/src/privacy/privacy_encoding_params.go
+++ b/src/bin/config_parser/src/privacy/privacy_encoding_params.go
@@ -156,7 +156,7 @@
 // {epsilon, population, sparsity, prob_bit_flip, num_index_points}
 func parsePrivacyEncodingRecord(record []string) (key paramsMapKey, params PrivacyEncodingParams, err error) {
 	if len(record) != 5 {
-		return key, params, fmt.Errorf("Wrong record size: %d", len(record))
+		return key, params, fmt.Errorf("wrong record size: %d", len(record))
 	}
 	epsilon, err := strconv.ParseFloat(record[0], 64)
 	if err != nil {
@@ -206,15 +206,18 @@
 	sort.Slice(*vals, func(i, j int) bool { return (*vals)[i] < (*vals)[j] })
 }
 
-// Given a |metric| and |report|, looks up the corresponding PrivacyEncodingParams from |calc|'s paramMap.
+// Given a |metric| and |report|, looks up the corresponding PrivacyEncodingParams from |calc|'s
+// paramMap. If |report| has an integer range size which is smaller than the number of index points
+// computed by |calc|, this function uses that integer range size as the NumIndexPoints field of the
+// returned PrivacyEncodingParams in order to avoid incurring unnecessary rounding error.
 //
-// If paramMap does not have a key which exactly matches the values drawn from |metric|, |report|, and |calc.constants|,
-// then parameters are returned for the closest key which provides at least as much privacy as is required by
-// |report|'s PrivacyLevel. See getBestMappedKey for more details.
+// If paramMap does not have a key which exactly matches the values drawn from |metric|, |report|,
+// and |calc.constants|, then parameters are returned for the closest key which provides at least as
+// much privacy as is required by |report|'s PrivacyLevel. See getBestMappedKey for more details.
 func (calc *PrivacyEncodingParamsCalculator) GetPrivacyEncodingParamsForReport(metric *config.MetricDefinition, report *config.ReportDefinition) (params PrivacyEncodingParams, err error) {
 	epsilon, ok := calc.Constants.EpsilonForPrivacyLevel[report.PrivacyLevel]
 	if !ok {
-		return params, fmt.Errorf("No epsilon found for privacy level: %v", report.PrivacyLevel)
+		return params, fmt.Errorf("no epsilon found for privacy level: %v", report.PrivacyLevel)
 	}
 
 	sparsity, err := getSparsityForReport(metric, report)
@@ -222,7 +225,21 @@
 		return params, err
 	}
 
-	return calc.GetPrivacyEncodingParams(epsilon, calc.Constants.population, sparsity)
+	range_size, err := getIntegerRangeSizeForReport(report)
+	if err != nil {
+		return params, err
+	}
+
+	params, err = calc.GetPrivacyEncodingParams(epsilon, calc.Constants.population, sparsity)
+	if err != nil {
+		return params, err
+	}
+
+	if range_size < uint64(params.NumIndexPoints) {
+		params.NumIndexPoints = uint32(range_size)
+	}
+
+	return params, err
 }
 
 // Given an |epsilon|, |population|, and |sparsity|, looks up the corresponding PrivacyEncodingParams from |calc|'s paramMap.
@@ -238,12 +255,61 @@
 
 	params, ok := calc.paramMap[key]
 	if !ok {
-		return params, fmt.Errorf("No params found for key: (epsilon=%f, population=%d, sparsity=%d)", key.epsilon, key.population, key.sparsity)
+		return params, fmt.Errorf("no params found for key: (epsilon=%f, population=%d, sparsity=%d)", key.epsilon, key.population, key.sparsity)
 	}
 
 	return calc.paramMap[key], nil
 }
 
+// Returns the number of valid integer values for |report|. For FleetwideOccurrenceCounts,
+// UniqueDeviceNumericStats, and HourlyValueNumericStats reports, this is the difference between
+// the report's MaxValue and MinValue fields. For FleetwideHistograms and StringCounts reports,
+// it is the report's MaxCount field.
+//
+// For UniqueDeviceCounts, UniqueDeviceHistograms, and HourlyValueHistograms reports, this notion
+// doesn't apply and the returned range size is 0.
+//
+// A FleetwideMeans report has two separate configured ranges: one for sum values and another for
+// count values. The returned range size is the maximum of the two range sizes.
+func getIntegerRangeSizeForReport(report *config.ReportDefinition) (range_size uint64, err error) {
+	switch report.ReportType {
+	case config.ReportDefinition_FLEETWIDE_OCCURRENCE_COUNTS,
+		config.ReportDefinition_UNIQUE_DEVICE_NUMERIC_STATS,
+		config.ReportDefinition_HOURLY_VALUE_NUMERIC_STATS:
+		{
+			size := report.MaxValue - report.MinValue
+			if size >= 0 {
+				range_size = uint64(size)
+			} else {
+				return range_size, fmt.Errorf("min value %d is larger than max value %d", report.MinValue, report.MaxValue)
+			}
+		}
+	case config.ReportDefinition_FLEETWIDE_HISTOGRAMS,
+		config.ReportDefinition_STRING_COUNTS:
+		range_size, err = report.MaxCount, nil
+	case config.ReportDefinition_FLEETWIDE_MEANS:
+		{
+			sum_size := report.MaxValue - report.MinValue
+			if sum_size >= 0 {
+				range_size = uint64(sum_size)
+			} else {
+				return range_size, fmt.Errorf("min value %d is larger than max value %d", report.MinValue, report.MaxValue)
+			}
+			if range_size < report.MaxCount {
+				range_size = report.MaxCount
+			}
+		}
+	case config.ReportDefinition_UNIQUE_DEVICE_COUNTS,
+		config.ReportDefinition_UNIQUE_DEVICE_HISTOGRAMS,
+		config.ReportDefinition_HOURLY_VALUE_HISTOGRAMS:
+		range_size = 0
+	default:
+		return range_size, fmt.Errorf("unsupported ReportType: %v", report.ReportType)
+	}
+	return range_size, nil
+
+}
+
 // Returns the sparsity for a given |metric| and |report|. This is the max number of elements in the index vector representation
 // of a contribution for |report|.
 func getSparsityForReport(metric *config.MetricDefinition, report *config.ReportDefinition) (sparsity uint64, err error) {
@@ -272,7 +338,7 @@
 		case config.ReportDefinition_AT_LEAST_ONCE:
 			numEventVectors, err = metric.EventCodeBufferMax, nil
 		default:
-			err = fmt.Errorf("Unexpected LocalAggregationProcedure: %v", report.LocalAggregationProcedure)
+			err = fmt.Errorf("unexpected LocalAggregationProcedure: %v", report.LocalAggregationProcedure)
 		}
 	case
 		config.ReportDefinition_FLEETWIDE_OCCURRENCE_COUNTS,
@@ -286,7 +352,7 @@
 		numEventVectors, err = metric.EventCodeBufferMax, nil
 
 	default:
-		err = fmt.Errorf("Unsupported ReportType: %v", report.ReportType)
+		err = fmt.Errorf("unsupported ReportType: %v", report.ReportType)
 	}
 	return numEventVectors, err
 }
@@ -317,7 +383,7 @@
 	case config.MetricDefinition_STRING:
 		numBuckets, err = numBuckets, fmt.Errorf("STRING metrics are not supported yet")
 	default:
-		err = fmt.Errorf("Unsupported metric type %v", metric.MetricType)
+		err = fmt.Errorf("unsupported metric type %v", metric.MetricType)
 	}
 	return numBuckets, err
 }
@@ -332,7 +398,7 @@
 	case nil:
 		err = fmt.Errorf("IntegerBuckets type not set")
 	default:
-		err = fmt.Errorf("Unexpected IntegerBuckets type")
+		err = fmt.Errorf("unexpected IntegerBuckets type")
 	}
 	return numBuckets, err
 }
@@ -367,14 +433,14 @@
 // Otherwise, the returned |mappedEpsilon| is the greatest epsilon value in |lists| which is less than or equal to |epsilon|.
 func (lists *paramsKeyLists) getBestMappedEpsilon(epsilon float64) (mappedEpsilon float64, err error) {
 	if len(lists.epsilons) == 0 {
-		return mappedEpsilon, fmt.Errorf("List of mapped epsilon values is empty")
+		return mappedEpsilon, fmt.Errorf("list of mapped epsilon values is empty")
 	}
 	i := sort.Search(len(lists.epsilons), func(i int) bool { return lists.epsilons[i] > epsilon })
 
 	if i > 0 {
 		return lists.epsilons[i-1], nil
 	} else {
-		return mappedEpsilon, fmt.Errorf("Input epsilon %v is outside the valid range", epsilon)
+		return mappedEpsilon, fmt.Errorf("input epsilon %v is outside the valid range", epsilon)
 	}
 }
 
@@ -382,14 +448,14 @@
 // Otherwise, the returned |mappedPopulation| is the greatest population value in |lists| which is less than or equal to |population|.
 func (lists *paramsKeyLists) getBestMappedPopulation(population uint64) (mappedPopulation uint64, err error) {
 	if len(lists.populations) == 0 {
-		return mappedPopulation, fmt.Errorf("List of mapped population values is empty")
+		return mappedPopulation, fmt.Errorf("list of mapped population values is empty")
 	}
 	i := sort.Search(len(lists.populations), func(i int) bool { return lists.populations[i] > population })
 
 	if i > 0 {
 		return lists.populations[i-1], nil
 	} else {
-		return mappedPopulation, fmt.Errorf("Input population %v is outside the valid range", population)
+		return mappedPopulation, fmt.Errorf("input population %v is outside the valid range", population)
 	}
 }
 
@@ -397,13 +463,13 @@
 // Otherwise, the returned |mappedSparsity| is the least sparsity value in |lists| which is greater than or equal to |sparsity|.
 func (lists *paramsKeyLists) getBestMappedSparsity(sparsity uint64) (mappedSparsity uint64, err error) {
 	if len(lists.sparsities) == 0 {
-		return mappedSparsity, fmt.Errorf("List of mapped sparsity values is empty")
+		return mappedSparsity, fmt.Errorf("list of mapped sparsity values is empty")
 	}
 	i := sort.Search(len(lists.sparsities), func(i int) bool { return lists.sparsities[i] >= sparsity })
 
 	if i < len(lists.sparsities) {
 		return lists.sparsities[i], nil
 	} else {
-		return mappedSparsity, fmt.Errorf("Input sparsity %v is outside the valid range", sparsity)
+		return mappedSparsity, fmt.Errorf("input sparsity %v is outside the valid range", sparsity)
 	}
 }
diff --git a/src/bin/config_parser/src/privacy/privacy_encoding_params_test.go b/src/bin/config_parser/src/privacy/privacy_encoding_params_test.go
index 48e380a..9db1d5d 100644
--- a/src/bin/config_parser/src/privacy/privacy_encoding_params_test.go
+++ b/src/bin/config_parser/src/privacy/privacy_encoding_params_test.go
@@ -82,6 +82,105 @@
 	}
 }
 
+func TestGetIntegerRangeForReport(t *testing.T) {
+	var minValue int64 = -5
+	var maxValue int64 = 10
+	var maxCount uint64 = 20
+	var smallMaxCount uint64 = 5
+
+	// Reports with MinValue and MaxValue set
+	fleetwideOccurrenceCountsReport := config.ReportDefinition{
+		ReportName: "FleetwideOccurrenceCounts",
+		ReportType: config.ReportDefinition_FLEETWIDE_OCCURRENCE_COUNTS,
+		MinValue:   minValue,
+		MaxValue:   maxValue,
+	}
+	uniqueDeviceNumericStatsReport := config.ReportDefinition{
+		ReportName: "UniqueDeviceNumericStats",
+		ReportType: config.ReportDefinition_UNIQUE_DEVICE_NUMERIC_STATS,
+		MinValue:   minValue,
+		MaxValue:   maxValue,
+	}
+	hourlyValueNumericStatsReport := config.ReportDefinition{
+		ReportName: "HourlyValueNumericStats",
+		ReportType: config.ReportDefinition_HOURLY_VALUE_NUMERIC_STATS,
+		MinValue:   minValue,
+		MaxValue:   maxValue,
+	}
+	// Reports with MaxCount set
+	fleetwideHistogramsReport := config.ReportDefinition{
+		ReportName: "FleetwideHistograms",
+		ReportType: config.ReportDefinition_FLEETWIDE_HISTOGRAMS,
+		MaxCount:   maxCount,
+	}
+	stringCountsReport := config.ReportDefinition{
+		ReportName: "StringCounts",
+		ReportType: config.ReportDefinition_STRING_COUNTS,
+		MaxCount:   maxCount,
+	}
+	// Reports with MinValue, MaxValue, and MaxCount set
+	fleetwideMeansLargerSumRangeReport := config.ReportDefinition{
+		ReportName: "FleetwideMeansLargerSumRange",
+		ReportType: config.ReportDefinition_FLEETWIDE_MEANS,
+		MinValue:   minValue,
+		MaxValue:   maxValue,
+		MaxCount:   smallMaxCount,
+	}
+	fleetwideMeansLargerCountRangeReport := config.ReportDefinition{
+		ReportName: "FleetwideMeansLargerCountRange",
+		ReportType: config.ReportDefinition_FLEETWIDE_MEANS,
+		MinValue:   minValue,
+		MaxValue:   maxValue,
+		MaxCount:   maxCount,
+	}
+	// Reports without an integer range
+	uniqueDeviceCountsReport := config.ReportDefinition{
+		ReportName: "UniqueDeviceCounts",
+		ReportType: config.ReportDefinition_UNIQUE_DEVICE_COUNTS,
+	}
+	uniqueDeviceHistogramsReport := config.ReportDefinition{
+		ReportName: "UniqueDeviceHistograms",
+		ReportType: config.ReportDefinition_UNIQUE_DEVICE_HISTOGRAMS,
+	}
+	hourlyValueHistogramsReport := config.ReportDefinition{
+		ReportName: "HourlyValueHistograms",
+		ReportType: config.ReportDefinition_HOURLY_VALUE_HISTOGRAMS,
+	}
+	// Invalid report type
+	unsetReportTypeReport := config.ReportDefinition{
+		ReportName: "UnsetReportType",
+	}
+	var tests = []struct {
+		report   *config.ReportDefinition
+		valid    bool
+		expected uint64
+	}{
+		// Valid input:
+		{&fleetwideOccurrenceCountsReport, true, uint64(maxValue - minValue)},
+		{&uniqueDeviceNumericStatsReport, true, uint64(maxValue - minValue)},
+		{&hourlyValueNumericStatsReport, true, uint64(maxValue - minValue)},
+		{&fleetwideHistogramsReport, true, maxCount},
+		{&stringCountsReport, true, maxCount},
+		{&fleetwideMeansLargerSumRangeReport, true, uint64(maxValue - minValue)},
+		{&fleetwideMeansLargerCountRangeReport, true, maxCount},
+		{&uniqueDeviceCountsReport, true, 0},
+		{&uniqueDeviceHistogramsReport, true, 0},
+		{&hourlyValueHistogramsReport, true, 0},
+		// Invalid input:
+		{&unsetReportTypeReport, false, 0},
+	}
+	for _, test := range tests {
+		result, err := getIntegerRangeSizeForReport(test.report)
+		if test.valid && err != nil {
+			t.Errorf("getIntegerRangeSizeForReport() failed for report %s: %v", test.report.ReportName, err)
+		} else if !test.valid && err == nil {
+			t.Errorf("getIntegerRangeSizeForReport() accepted invalid report: %s", test.report.ReportName)
+		} else if test.valid && result != test.expected {
+			t.Errorf("getIntegerRangeSizeForReport() for report %s: expected %d, got %d", test.report.ReportName, test.expected, result)
+		}
+	}
+}
+
 func TestGetSparsityForReport(t *testing.T) {
 	var eventCodeBufferMax uint64 = 5
 	var stringBufferMax uint32 = 3
@@ -417,11 +516,30 @@
 		t.Errorf("NewPrivacyEncodingParamsCalculatorForTesting failed with valid input. Error message: %v", err)
 	}
 
-	metric := config.MetricDefinition{
-		MetricType:         config.MetricDefinition_OCCURRENCE,
-		EventCodeBufferMax: 5,
+	var eventCodeBufferMax uint64 = 5
+	var minValue int64 = 0
+	var smallMaxValue int64 = 1
+	var largeMaxValue int64 = 100
+	var smallMaxCount uint64 = 1
+	var largeMaxCount uint64 = 100
+	var numLinearBuckets uint32 = 2
+
+	linearBuckets := config.LinearIntegerBuckets{NumBuckets: numLinearBuckets}
+	buckets := config.IntegerBuckets{
+		Buckets: &config.IntegerBuckets_Linear{&linearBuckets},
 	}
 
+	// Metrics
+	occurrenceMetric := config.MetricDefinition{
+		MetricType:         config.MetricDefinition_OCCURRENCE,
+		EventCodeBufferMax: eventCodeBufferMax,
+	}
+	integerMetric := config.MetricDefinition{
+		MetricType:         config.MetricDefinition_INTEGER,
+		EventCodeBufferMax: eventCodeBufferMax,
+	}
+
+	// Reports
 	highPrivacyAtLeastOnceReport := config.ReportDefinition{
 		ReportName:                "HighPrivacyAtLeastOnceReport",
 		ReportType:                config.ReportDefinition_UNIQUE_DEVICE_COUNTS,
@@ -434,6 +552,34 @@
 		PrivacyLevel:              config.ReportDefinition_LOW_PRIVACY,
 		LocalAggregationProcedure: config.ReportDefinition_SELECT_FIRST,
 	}
+	smallRangeSizeFleetwideOccurrenceCountsReport := config.ReportDefinition{
+		ReportName:   "smallRangeSizeFleetwideOccurrenceCountsReport",
+		ReportType:   config.ReportDefinition_FLEETWIDE_OCCURRENCE_COUNTS,
+		PrivacyLevel: config.ReportDefinition_LOW_PRIVACY,
+		MinValue:     minValue,
+		MaxValue:     smallMaxValue,
+	}
+	largeRangeSizeFleetwideOccurrenceCountsReport := config.ReportDefinition{
+		ReportName:   "largeRangeSizeFleetwideOccurrenceCountsReport",
+		ReportType:   config.ReportDefinition_FLEETWIDE_OCCURRENCE_COUNTS,
+		PrivacyLevel: config.ReportDefinition_LOW_PRIVACY,
+		MinValue:     minValue,
+		MaxValue:     largeMaxValue,
+	}
+	smallRangeSizeFleetwideHistogramsReport := config.ReportDefinition{
+		ReportName:   "smallRangeSizeFleetwideHistogramsReport",
+		ReportType:   config.ReportDefinition_FLEETWIDE_HISTOGRAMS,
+		IntBuckets:   &buckets,
+		PrivacyLevel: config.ReportDefinition_LOW_PRIVACY,
+		MaxCount:     smallMaxCount,
+	}
+	largeRangeSizeFleetwideHistogramsReport := config.ReportDefinition{
+		ReportName:   "largeRangeSizeFleetwideHistogramsReport",
+		ReportType:   config.ReportDefinition_FLEETWIDE_HISTOGRAMS,
+		IntBuckets:   &buckets,
+		PrivacyLevel: config.ReportDefinition_LOW_PRIVACY,
+		MaxCount:     largeMaxCount,
+	}
 	unsetReportTypeReport := config.ReportDefinition{
 		ReportName: "UnsetReportTypeReport",
 	}
@@ -448,14 +594,45 @@
 		expected PrivacyEncodingParams
 	}{
 		// Valid input:
-		// The target epsilon is 1.0 and the sparsity is metric.EventCodeBufferMax. The best-match key is {1.0, 10000, 10}.
-		{args{&metric, &highPrivacyAtLeastOnceReport}, true, PrivacyEncodingParams{0.019537480548024178, 4}},
+		// The target epsilon is 1.0 and the sparsity is |eventCodeBufferMax|. The best-match key is
+		// {1.0, 10000, 10}. The report has type UniqueDeviceCounts, so the NumIndexPoints field of
+		// PrivacyEncodingParams is set to 0.
+		{args{&occurrenceMetric, &highPrivacyAtLeastOnceReport}, true,
+			PrivacyEncodingParams{0.019537480548024178, 0}},
 		// The target epsilon is 10.0 and the sparsity is 1. The best-match key is {5.0, 10000, 1}.
-		{args{&metric, &lowPrivacySelectFirstReport}, true, PrivacyEncodingParams{0.000906200148165226, 12}},
+		// The report has type UniqueDeviceCounts, so the NumIndexPoints field of PrivacyEncodingParams is
+		// set to 0.
+		{args{&occurrenceMetric, &lowPrivacySelectFirstReport}, true,
+			PrivacyEncodingParams{0.000906200148165226, 0}},
+		// The target epsilon is 10.0 and the sparsity is |eventCodeBufferMax|.
+		// The best-match key is {5.0, 10000, 10}.
+		// The integer range size of the report (= |smallMaxValue|) is smaller than the number of index
+		// points computed by |calc|, so the returned NumIndexPoints should be equal to |smallMaxValue|.
+		{args{&occurrenceMetric, &smallRangeSizeFleetwideOccurrenceCountsReport}, true,
+			PrivacyEncodingParams{0.0014028092846274376, uint32(smallMaxValue)}},
+		// The target epsilon is 10.0 and the sparsity is |eventCodeBufferMax|.
+		// The best-match key is {5.0, 10000, 10}.
+		// The integer range size of the report (= largeMaxValue) is larger than the number of index points
+		// computed by |calc|, so the returned NumIndexPoints is given by the value at the best-match key.
+		{args{&occurrenceMetric, &largeRangeSizeFleetwideOccurrenceCountsReport}, true,
+			PrivacyEncodingParams{0.0014028092846274376, 10}},
+		// The target epsilon is 10.0 and the sparsity is |eventCodeBufferMax| * |numLinearBuckets|.
+		// The best-match key is {5.0, 10000, 10}.
+		// The integer range size of the report (= |smallMaxCount|) is smaller than the number of index
+		// points computed by |calc|, so the returned NumIndexPoints should be equal to |smallMaxCount|.
+		{args{&integerMetric, &smallRangeSizeFleetwideHistogramsReport}, true,
+			PrivacyEncodingParams{0.0014028092846274376, uint32(smallMaxCount)}},
+		// The target epsilon is 10.0 and the sparsity is |eventCodeBufferMax| * |numLinearBuckets|.
+		// The best-match key is {5.0, 10000, 10}.
+		// The integer range size of the report (= |largeMaxCount|) is larger than the number of index
+		// points computed by |calc|, so the returned NumIndexPoints is given by the value at the best-match
+		// key.
+		{args{&integerMetric, &largeRangeSizeFleetwideHistogramsReport}, true,
+			PrivacyEncodingParams{0.0014028092846274376, 10}},
 
 		// Invalid input:
 		// This report does not have a report type set.
-		{args{&metric, &unsetReportTypeReport}, false, PrivacyEncodingParams{}},
+		{args{&occurrenceMetric, &unsetReportTypeReport}, false, PrivacyEncodingParams{}},
 	}
 	for _, test := range tests {
 		result, err := calc.GetPrivacyEncodingParamsForReport(test.input.metric, test.input.report)