All files bucketer.ts

94.11% Statements 32/34
92.85% Branches 13/14
100% Functions 3/3
94.11% Lines 32/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88      3x 3x               3x 3x   3x   3x 175x 175x   175x           3x                   3x             172x         172x 160x 160x 12x 5x 5x 7x 7x 7x             172x   172x 184x   184x 16x     168x 157x     11x 7x         172x   172x    
import type { Context, AttributeValue, FeatureKey, BucketBy } from "@featurevisor/types";
 
import { Logger } from "./logger";
import { getValueFromContext } from "./conditions";
import { MurmurHashV3 } from "./murmurhash";
 
export type BucketKey = string;
export type BucketValue = number; // 0 to 100,000 (100% * 1000 to include three decimal places in same integer)
 
/**
 * Generic hashing
 */
const HASH_SEED = 1;
const MAX_HASH_VALUE = Math.pow(2, 32);
 
export const MAX_BUCKETED_NUMBER = 100000; // 100% * 1000 to include three decimal places in the same integer value
 
export function getBucketedNumber(bucketKey: string): BucketValue {
  const hashValue = MurmurHashV3(bucketKey, HASH_SEED);
  const ratio = hashValue / MAX_HASH_VALUE;
 
  return Math.floor(ratio * MAX_BUCKETED_NUMBER);
}
 
/**
 * Bucket key
 */
const DEFAULT_BUCKET_KEY_SEPARATOR = ".";
 
export interface GetBucketKeyOptions {
  featureKey: FeatureKey;
  bucketBy: BucketBy;
  context: Context;
 
  logger: Logger;
}
 
export function getBucketKey(options: GetBucketKeyOptions): BucketKey {
  const {
    featureKey,
    bucketBy,
    context,
 
    logger,
  } = options;
 
  let type;
  let attributeKeys;
 
  if (typeof bucketBy === "string") {
    type = "plain";
    attributeKeys = [bucketBy];
  } else if (Array.isArray(bucketBy)) {
    type = "and";
    attributeKeys = bucketBy;
  } else if (typeof bucketBy === "object" && Array.isArray(bucketBy.or)) {
    type = "or";
    attributeKeys = bucketBy.or;
  } else E{
    logger.error("invalid bucketBy", { featureKey, bucketBy });
 
    throw new Error("invalid bucketBy");
  }
 
  const bucketKey: AttributeValue[] = [];
 
  attributeKeys.forEach((attributeKey) => {
    const attributeValue = getValueFromContext(context, attributeKey);
 
    if (typeof attributeValue === "undefined") {
      return;
    }
 
    if (type === "plain" || type === "and") {
      bucketKey.push(attributeValue);
    } else {
      // or
      if (bucketKey.length === 0) {
        bucketKey.push(attributeValue);
      }
    }
  });
 
  bucketKey.push(featureKey);
 
  return bucketKey.join(DEFAULT_BUCKET_KEY_SEPARATOR);
}