• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

caleb531 / workday-time-calculator / 31070267121

06 Aug 2026 04:05AM UTC coverage: 81.373% (-0.07%) from 81.44%
31070267121

push

github

caleb531
Add test for retention of object-key order

For autocompletion algorithm.

422 of 539 branches covered (78.29%)

Branch coverage included in aggregate %.

1024 of 1238 relevant lines covered (82.71%)

287.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.62
/scripts/autocompletion-worker.js
1
import * as idbKeyval from 'idb-keyval';
2
import { countBy, maxBy } from 'es-toolkit';
3

4
// A map representing the various algorithms for the autocomplete; each key
5
// name is the ID of a specific autocomplete mode, and each value is a function
6
// which returns a dynamically-constructed regular expression for that mode;
7
// each callback receives the (regex-escaped) current query substring as a
8
// parameter
9
const modeRegexes = {
132✔
10
  lazy: (q) => new RegExp('\\b' + q + '\\S*\\b', 'g'),
194✔
11
  greedy: (q) => new RegExp('\\b' + q + '\\S*( \\S+){0,2}\\b', 'g')
92✔
12
};
13

14
// Escape all characters that have special meaning in regular expressions
15
function escapeRegExp(str) {
16
  return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
442✔
17
}
18

19
// Retrieve the current date in YYYY-M-D format (e.g. 2022-3-4)
20
function getCurrentDate() {
21
  const date = new Date();
290✔
22
  return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
290✔
23
}
24

25
// Process all entered log entries and store the keywords into a string, where
26
// each line of text is separated by a newline
27
async function processLogEntries() {
28
  const entries = await idbKeyval.entries();
132✔
29
  return (
131✔
30
    entries
31
      .filter(([key]) => /^wtc-date-/.test(key))
263✔
32
      .map(([, value]) => {
33
        return value.ops
233✔
34
          .filter((op) => op.insert.trim())
4,371✔
35
          .map((op) => op.insert)
2,187✔
36
          .join('\n');
37
      })
38
      .join('\n')
39
      // Collapse consecutive sequences of spaces
40
      .replace(/ +/gi, ' ')
41
  );
42
}
43

44
// When the app needs to autocomplete, it sends the web worker a list of all
45
// words on the current line, up to (but not including) the character
46
// immediately following the text cursor (e.g. "send email corre"); all of
47
// these words may not be relevant, but we know that a match based on more
48
// words is more relevant than a match based on fewer; therefore, we retrieve a
49
// list of all substring sequences of words in the given query string (e.g. if
50
// the query is "send email correspondence to", then the resulting array would
51
// be ["send email correspondence to", "email correspondence to",
52
// "correspondence to", "correspondence"])
53
function getQuerySubstrings(query) {
54
  const queryWords = query.split(' ');
158✔
55
  return queryWords.map((queryWord, i) => {
158✔
56
    return queryWords.slice(i).join(' ');
286✔
57
  });
58
}
59

60
// Convert the above array of query substrings to regular expressions that can
61
// be used to find matching completions within the keyword string
62
function convertQuerySubstringsToRegexes(querySubstrings, autocompleteMode) {
63
  return querySubstrings.map((querySubstring) => {
158✔
64
    return modeRegexes[autocompleteMode](escapeRegExp(querySubstring));
286✔
65
  });
66
}
67

68
// Given a completion, subtract of the start of the completion (which should be
69
// the same as the given query substring) and return the remaining part of the
70
// string; the removal should ignore case so that the case of the original
71
// completion is preserved
72
function getCompletionPlaceholderFromQuery(completion, query) {
73
  const substringReplacementRegex = new RegExp(escapeRegExp(query), 'i');
156✔
74
  return completion.replace(substringReplacementRegex, '');
156✔
75
}
76

77
// Build list of possible completions given the last few words preceding the
78
// user's cursor (what we call "the completion query", or simply "the query")
79
function buildCompletions({ keywordStr, completionQuery, autocompleteMode }) {
80
  const querySubstrings = getQuerySubstrings(completionQuery);
158✔
81
  const substringRegexes = convertQuerySubstringsToRegexes(
158✔
82
    querySubstrings,
83
    autocompleteMode
84
  );
85
  const substringMatchGroups = substringRegexes.map((substringRegex) => {
158✔
86
    return keywordStr.match(substringRegex) || [];
286✔
87
  });
88
  // We use a 'for' loop so we can short-circuit and return a proper result
89
  // object as soon as we find a matching completion; we do this as opposed to
90
  // using something like Array.prototype.find() so we can not only
91
  // short-circuit, but also transform the value at the same time without
92
  // having to re-process the matches
93
  for (let i = 0; i < substringMatchGroups.length; i += 1) {
158✔
94
    const matchGroup = substringMatchGroups[i];
158✔
95
    const querySubstring = querySubstrings[i];
158✔
96
    // Retrieve all phrases in the keyword string that match the given
97
    // autocomplete query; we first map the number of occurrences of each match
98
    const countPairs = Object.entries(countBy(matchGroup, (match) => match));
308✔
99
    // Then we retrieve the match with the most occurrences
100
    const matchingCompletion = maxBy(countPairs, ([, count]) => count)?.[0];
158✔
101
    if (matchingCompletion) {
158✔
102
      return {
156✔
103
        matchingCompletion: matchingCompletion,
104
        completionPlaceholder: getCompletionPlaceholderFromQuery(
105
          matchingCompletion,
106
          querySubstring
107
        )
108
      };
109
    }
110
  }
111
  return {
2✔
112
    matchingCompletion: '',
113
    completionPlaceholder: ''
114
  };
115
}
116

117
// Process log entries as soon as the web worker is initially loaded (before
118
// any messages are sent/received)
119
let entriesPromise = processLogEntries();
132✔
120
// Keep track of when we last ran an autocomplete search so we can update the
121
// available autocomplete data as needed (see comment below)
122
let lastAutocompleteDate = getCurrentDate();
132✔
123
// Wait for all log entries to be processed before handling messages from the
124
// main thread
125
self.onmessage = async (event) => {
132✔
126
  // If the user never closes Workday Time Calculator, make sure the available
127
  // autocomplete data still refreshes itself when the current date changes
128
  const currentDate = getCurrentDate();
158✔
129
  if (currentDate !== lastAutocompleteDate) {
158!
130
    lastAutocompleteDate = currentDate;
×
131
    entriesPromise = processLogEntries();
×
132
  }
133
  const keywordStr = await entriesPromise;
158✔
134
  // Echo the caller's ID so it can discard this response if a newer query has
135
  // been issued while the worker was awaiting the local history index
136
  const requestId = event.data.requestId;
158✔
137
  self.postMessage({
158✔
138
    requestId: requestId,
139
    ...buildCompletions({
140
      keywordStr: keywordStr,
141
      completionQuery: event.data.completionQuery,
142
      autocompleteMode: event.data.autocompleteMode
143
    })
144
  });
145
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc