All files / src/pages/flowsheet-page/flowsheet/LeftSideBar/Formulas aggregateFormula.ts

100% Statements 26/26
85.71% Branches 6/7
100% Functions 3/3
100% Lines 24/24

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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140          76x 76x             76x 76x                       76x 76x                           380x                         12x 12x 60x               29x                       29x 29x           29x 2x   2x                             29x 29x   27x     2x                     29x                       267x       9x   2x      
// Frontend aggregate grammar for property-key formulas:
//   FUNCTION(@property_key)
//
// FUNCTION is one of SUM/AVG/MIN/MAX/COUNT. property_key is the stable
// object-schema key, including dotted keys such as "sink.heat".
const AGGREGATE_FUNCTIONS = ["SUM", "AVG", "MIN", "MAX", "COUNT"] as const;
const AGGREGATE_KEY_PATTERN = "[A-Za-z_][A-Za-z0-9_.]*";
 
/**
 * `react-mentions` custom triggers must expose:
 * - group 1: the text range to replace
 * - group 2: the query used to filter suggestions
 */
export const AGGREGATE_FUNCTION_TRIGGER = /(?:^|[^\w@.])(([A-Za-z]{1,5}))$/;
export const AGGREGATE_KEY_TRIGGER = new RegExp(
  `\\b(?:${AGGREGATE_FUNCTIONS.join("|")})\\s*\\(\\s*(@((${AGGREGATE_KEY_PATTERN})?))$`,
  "i",
);
 
/**
 * Temporary react-mentions markup for aggregate snippets.
 *
 * Aggregate selections must persist as plain formula text, not as property
 * mentions. A bare "__id__" markup looks tempting, but react-mentions parses
 * that as "any text is a mention", which breaks ordinary `@Pump` suggestions.
 */
export const AGGREGATE_SUGGESTION_MARKUP = "{{__id__}}";
const AGGREGATE_SUGGESTION_MARKUP_PATTERN = /\{\{([^}]+)\}\}/g;
 
export type AggregateSuggestion = {
  id: string;
  display: string;
};
 
export type AggregateQueryRange = {
  query: string;
  start: number;
  end: number;
};
 
export const aggregateFunctionOptions: AggregateSuggestion[] =
  AGGREGATE_FUNCTIONS.map((functionName) => ({
    id: `${functionName}(@`,
    display: `${functionName}(@key)`,
  }));
 
/**
 * Return aggregate function snippets matching the partial token immediately
 * before the caret. The inserted text intentionally includes "(@" so choosing
 * a function moves the user straight into property-key completion.
 */
export function getAggregateFunctionOptions(
  query: string,
): AggregateSuggestion[] {
  const normalizedQuery = query.toUpperCase();
  return aggregateFunctionOptions.filter(({ id }) =>
    id.startsWith(normalizedQuery),
  );
}
 
export function isCursorInAggregateKeyContext(
  value: string,
  cursorPosition: number,
): boolean {
  return getAggregateKeyQuery(value, cursorPosition) !== null;
}
 
/**
 * Detect the property-key portion of calls like SUM(@work_mechanical) or
 * SUM(@sink.heat). The returned range is expressed in plain-text input
 * coordinates, not react-mentions markup coordinates.
 */
export function getAggregateKeyQuery(
  value: string,
  cursorPosition: number,
): AggregateQueryRange | null {
  const prefix = value.slice(0, cursorPosition);
  const match = prefix.match(
    new RegExp(
      `\\b(?:${AGGREGATE_FUNCTIONS.join("|")})\\s*\\(\\s*@(${AGGREGATE_KEY_PATTERN})?$`,
      "i",
    ),
  );
  if (!match) return null;
  const query = match[1] ?? "";
 
  return {
    query,
    start: cursorPosition - query.length,
    end: cursorPosition,
  };
}
 
/**
 * Detect a partial aggregate function name without stealing ordinary property
 * mentions or dotted property-key completions from react-mentions.
 */
export function getAggregateFunctionQuery(
  value: string,
  cursorPosition: number,
): AggregateQueryRange | null {
  const prefix = value.slice(0, cursorPosition);
  const match = prefix.match(/(?:^|[^\w@.])([A-Za-z]{1,5})$/);
  if (!match || getAggregateFunctionOptions(match[1]).length === 0) {
    return null;
  }
 
  return {
    query: match[1],
    start: cursorPosition - match[1].length,
    end: cursorPosition,
  };
}
 
export function isCursorInAggregateFunctionContext(
  value: string,
  cursorPosition: number,
): boolean {
  return getAggregateFunctionQuery(value, cursorPosition) !== null;
}
 
/**
 * Complete a selected property key and close the aggregate call, while avoiding
 * a duplicate ")" when replacing text before an existing close parenthesis.
 */
export function getAggregateKeyInsertionText(
  replacement: string,
  value: string,
  queryEnd: number,
): string {
  return value[queryEnd] === ")" ? replacement : `${replacement})`;
}
 
export function unwrapAggregateSuggestionMarkup(value: string): string {
  return value.replace(
    AGGREGATE_SUGGESTION_MARKUP_PATTERN,
    (_match, insertedText: string) => insertedText,
  );
}