From 4244d4183eddb81df0fe2bbbd83ad97861667940 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 12 Oct 2023 17:53:58 -0400 Subject: [PATCH] initial attempt at pattern-de-duplication based on subsequence detection --- __tests__/util/state.js | 19 ++++++++++++++++++- lib/actions/apiV2.js | 29 ++++++++++++++++++++++++++++- lib/util/state.js | 21 +++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/__tests__/util/state.js b/__tests__/util/state.js index 089ef09d0..f3fafdf27 100644 --- a/__tests__/util/state.js +++ b/__tests__/util/state.js @@ -1,9 +1,26 @@ /* globals describe, expect, it */ import '../test-utils/mock-window-url' -import { queryIsValid } from '../../lib/util/state' +import { isValidSubsequence, queryIsValid } from '../../lib/util/state' describe('util > state', () => { + describe('isValidSubsequence', () => { + it('should handle edge cases correctly', () => { + expect(isValidSubsequence([0], [0])).toBe(true) + expect(isValidSubsequence([0], [1])).toBe(false) + expect(isValidSubsequence([], [])).toBe(true) + expect(isValidSubsequence([], [9])).toBe(false) + expect(isValidSubsequence([9], [])).toBe(true) + expect(isValidSubsequence([9], [9, 9])).toBe(false) + expect(isValidSubsequence([9, 9, 9], [9, 9])).toBe(true) + }) + it('should handle normal cases correctly', () => { + expect(isValidSubsequence([1, 2, 3, 4, 5], [5, 6, 3])).toBe(false) + expect(isValidSubsequence([1, 2, 3, 4, 5], [2, 3, 4])).toBe(true) + expect(isValidSubsequence([1, 2, 4, 4, 3], [2, 3, 4])).toBe(false) + expect(isValidSubsequence([1, 2, 3, 4, 5], [1, 3, 4])).toBe(false) + }) + }) describe('queryIsValid', () => { const fakeFromLocation = { lat: 12, diff --git a/lib/actions/apiV2.js b/lib/actions/apiV2.js index ae64d539c..c4bf3ef0b 100644 --- a/lib/actions/apiV2.js +++ b/lib/actions/apiV2.js @@ -12,6 +12,7 @@ import { generateModeSettingValues } from '../util/api' import { getActiveItineraries, getActiveItinerary, + isValidSubsequence, queryIsValid } from '../util/state' import { ItineraryView } from '../util/ui' @@ -651,7 +652,33 @@ export const findRoute = (params) => const newRoute = clone(route) const routePatterns = {} - newRoute.patterns.forEach((pattern) => { + + // Sort patterns by length to make algorithm below more efficient + const patternsSortedByLength = newRoute.patterns.sort( + (a, b) => a.stops.length - b.stops.length + ) + + // Remove all patterns that are subsets of larger patterns + const filteredPatterns = patternsSortedByLength + // Start with the largest for performance + .reverse() + .filter((pattern) => { + // Compare to all other patterns TODO: make this beat O(n^2) + return !patternsSortedByLength.find((p) => { + // Don't compare against ourself + if (p.id === pattern.id) return false + + // If our pattern is longer, it's not a subset + if (p.stops.length < pattern.stops.length) return false + + return isValidSubsequence( + p.stops.map((s) => s.id), + pattern.stops.map((s) => s.id) + ) + }) + }) + + filteredPatterns.forEach((pattern) => { const patternStops = pattern.stops.map((stop) => { const color = stop.routes?.length > 0 && diff --git a/lib/util/state.js b/lib/util/state.js index c25b0b615..eac0ef907 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -886,3 +886,24 @@ export function getOperatorAndRoute(routeObject, transitOperators, intl) { ) ) } + +/** + * Helper method returns true if an array is a subsequence of another. + * + * More efficient than comparing strings as we don't need to look at the entire + * array. + */ +export function isValidSubsequence(array, sequence) { + // Find starting point + let i = 0 + let j = 0 + while (array[i] !== sequence[j] && i < array.length) { + i = i + 1 + } + // We've found the starting point, now we test to see if the rest of the sequence is matched + while (array[i] === sequence[j] && i < array.length && j < sequence.length) { + i = i + 1 + j = j + 1 + } + return j === sequence.length +}