Refactoring: move functions out of Utils

This commit is contained in:
Pieter Vander Vennet 2025-08-26 03:18:25 +02:00
parent 442f5a2923
commit 5ea2414204
16 changed files with 126 additions and 145 deletions

View file

@ -22,7 +22,6 @@ import ThemeViewState from "../src/Models/ThemeViewState"
import Validators from "../src/UI/InputElement/Validators" import Validators from "../src/UI/InputElement/Validators"
import questions from "../public/assets/generated/layers/questions.json" import questions from "../public/assets/generated/layers/questions.json"
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
import { Utils } from "../src/Utils"
import { TagUtils } from "../src/Logic/Tags/TagUtils" import { TagUtils } from "../src/Logic/Tags/TagUtils"
import Script from "./Script" import Script from "./Script"
import { Changes } from "../src/Logic/Osm/Changes" import { Changes } from "../src/Logic/Osm/Changes"
@ -562,7 +561,7 @@ export class GenerateDocs extends Script {
item.url.startsWith("pmtiles://") item.url.startsWith("pmtiles://")
) )
) )
const serverInfos = Utils.DedupOnId(serverInfosDupl, (item) => item.url) const serverInfos = Lists.dedupOnId(serverInfosDupl, (item) => item.url)
const titles = Lists.dedup(Lists.noEmpty(serverInfos.map((s) => s.category))) const titles = Lists.dedup(Lists.noEmpty(serverInfos.map((s) => s.category)))
titles.sort() titles.sort()

View file

@ -327,7 +327,7 @@ export class GenerateLicenseInfo extends Script {
} }
licenses.sort((a, b) => (a.path < b.path ? -1 : 1)) licenses.sort((a, b) => (a.path < b.path ? -1 : 1))
licenses = Utils.DedupOnId(licenses, (l) => l.path) licenses = Lists.dedupOnId(licenses, (l) => l.path)
const path = dir + "/license_info.json" const path = dir + "/license_info.json"
if (licenses.length === 0) { if (licenses.length === 0) {
console.log("Removing", path, "as it is empty") console.log("Removing", path, "as it is empty")

View file

@ -6,7 +6,6 @@ import { ImmutableStore, Store, Stores } from "../UIEventSource"
import ImageProvider, { ProvidedImage } from "./ImageProvider" import ImageProvider, { ProvidedImage } from "./ImageProvider"
import { WikidataImageProvider } from "./WikidataImageProvider" import { WikidataImageProvider } from "./WikidataImageProvider"
import Panoramax from "./Panoramax" import Panoramax from "./Panoramax"
import { Utils } from "../../Utils"
import { ServerSourceInfo } from "../../Models/SourceOverview" import { ServerSourceInfo } from "../../Models/SourceOverview"
import { Lists } from "../../Utils/Lists" import { Lists } from "../../Utils/Lists"
@ -151,7 +150,7 @@ export default class AllImageProviders {
} }
const source = Stores.concat(allSources).map((result) => { const source = Stores.concat(allSources).map((result) => {
const all = result.flatMap((x) => x) const all = result.flatMap((x) => x)
return Utils.DedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id]) return Lists.dedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id])
}) })
this._cachedImageStores[cachekey] = source this._cachedImageStores[cachekey] = source
return source return source

View file

@ -10,6 +10,7 @@ import { Point } from "geojson"
import { ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist" import { ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist"
import { Mapillary } from "../ImageProviders/Mapillary" import { Mapillary } from "../ImageProviders/Mapillary"
import { ServerSourceInfo } from "../../Models/SourceOverview" import { ServerSourceInfo } from "../../Models/SourceOverview"
import { Lists } from "../../Utils/Lists"
interface ImageFetcher { interface ImageFetcher {
/** /**
@ -465,6 +466,6 @@ export class CombinedFetcher {
this.fetchImage(source, lat, lon, state, sink) this.fetchImage(source, lat, lon, state, sink)
} }
return { images: sink.mapD((imgs) => Utils.DedupOnId(imgs, (i) => i["id"])), state } return { images: sink.mapD((imgs) => Lists.dedupOnId(imgs, (i) => i["id"])), state }
} }
} }

View file

@ -10,7 +10,9 @@ import ThemeConfig from "../../src/Models/ThemeConfig/ThemeConfig"
import { ThemeConfigJson } from "../../src/Models/ThemeConfig/Json/ThemeConfigJson" import { ThemeConfigJson } from "../../src/Models/ThemeConfig/Json/ThemeConfigJson"
import SpecialVisualizations from "../../src/UI/SpecialVisualizations" import SpecialVisualizations from "../../src/UI/SpecialVisualizations"
import ValidationUtils from "../../src/Models/ThemeConfig/Conversion/ValidationUtils" import ValidationUtils from "../../src/Models/ThemeConfig/Conversion/ValidationUtils"
import { QuestionableTagRenderingConfigJson } from "../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" import {
QuestionableTagRenderingConfigJson
} from "../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { LayerConfigJson } from "../../src/Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../src/Models/ThemeConfig/Json/LayerConfigJson"
import { Lists } from "../Utils/Lists" import { Lists } from "../Utils/Lists"
@ -206,7 +208,7 @@ export class SourceOverview {
} }
urls = urls.filter((item) => !!item?.url) urls = urls.filter((item) => !!item?.url)
urls.sort((a, b) => (a < b ? -1 : 1)) urls.sort((a, b) => (a < b ? -1 : 1))
urls = Utils.DedupOnId(urls, (item) => item.url) urls = Lists.dedupOnId(urls, (item) => item.url)
this.eliUrlsCached = urls this.eliUrlsCached = urls
return urls return urls
} }

View file

@ -1,18 +1,6 @@
import { import { Concat, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault } from "./Conversion"
Concat,
DesugaringContext,
DesugaringStep,
Each,
FirstOf,
Fuse,
On,
SetDefault,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import { import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
MinimalTagRenderingConfigJson,
TagRenderingConfigJson,
} from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson" import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations" import SpecialVisualizations from "../../../UI/SpecialVisualizations"
@ -1163,7 +1151,9 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
"Fully prepares and expands a layer for the LayerConfig.", "Fully prepares and expands a layer for the LayerConfig.",
new DeriveSource(), new DeriveSource(),
new On("tagRenderings", new Each(new RewriteSpecial())), new On("tagRenderings", new Each(new RewriteSpecial())),
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)), new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(function <T>(list: (T | T[])[]): T[] {
return Lists.flatten(list)
})),
new On( new On(
"tagRenderings", "tagRenderings",
(layer) => (layer) =>
@ -1180,7 +1170,9 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new On< new On<
(LineRenderingConfigJson | RewritableConfigJson<LineRenderingConfigJson>)[], (LineRenderingConfigJson | RewritableConfigJson<LineRenderingConfigJson>)[],
LayerConfigJson LayerConfigJson
>("lineRendering", new Each(new ExpandRewrite()).andThenF(Utils.Flatten)), >("lineRendering", new Each(new ExpandRewrite()).andThenF(function <T>(list: (T | T[])[]): T[] {
return Lists.flatten(list)
})),
new On<PointRenderingConfigJson[], LayerConfigJson>( new On<PointRenderingConfigJson[], LayerConfigJson>(
"pointRendering", "pointRendering",
(layer) => (layer) =>

View file

@ -178,9 +178,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
{ {
// Check for multiple, identical builtin questions - usability for studio users // Check for multiple, identical builtin questions - usability for studio users
const duplicates = Utils.Duplicates( const duplicates = Lists.duplicates(<string[]>json.tagRenderings.filter((tr) => typeof tr === "string"))
<string[]>json.tagRenderings.filter((tr) => typeof tr === "string")
)
for (let i = 0; i < json.tagRenderings.length; i++) { for (let i = 0; i < json.tagRenderings.length; i++) {
const tagRendering = json.tagRenderings[i] const tagRendering = json.tagRenderings[i]
if (typeof tagRendering === "string" && duplicates.indexOf(tagRendering) > 0) { if (typeof tagRendering === "string" && duplicates.indexOf(tagRendering) > 0) {
@ -209,7 +207,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
{ {
// duplicate ids in tagrenderings check // duplicate ids in tagrenderings check
const duplicates = Lists.noNull( const duplicates = Lists.noNull(
Utils.Duplicates(json.tagRenderings?.map((tr) => tr?.["id"])) Lists.duplicates(json.tagRenderings?.map((tr) => tr?.["id"]))
) )
if (duplicates?.length > 0) { if (duplicates?.length > 0) {
// It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list // It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list
@ -312,9 +310,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
) )
} }
const duplicateIds = Utils.Duplicates( const duplicateIds = Lists.duplicates((json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions"))
(json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions")
)
if (duplicateIds.length > 0 && !Utils.runningFromConsole) { if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
context context
.enter("tagRenderings") .enter("tagRenderings")

View file

@ -7,6 +7,7 @@ import ThemeConfig from "../ThemeConfig"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import { DetectDuplicatePresets, DoesImageExist, ValidateLanguageCompleteness } from "./Validation" import { DetectDuplicatePresets, DoesImageExist, ValidateLanguageCompleteness } from "./Validation"
import Constants from "../../Constants" import Constants from "../../Constants"
import { Lists } from "../../../Utils/Lists"
export class ValidateTheme extends DesugaringStep<ThemeConfigJson> { export class ValidateTheme extends DesugaringStep<ThemeConfigJson> {
/** /**
@ -105,7 +106,7 @@ export class ValidateTheme extends DesugaringStep<ThemeConfigJson> {
} }
this._validateImage.convert(theme.icon, context.enter("icon")) this._validateImage.convert(theme.icon, context.enter("icon"))
} }
const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"])) const dups = Lists.duplicates(json.layers.map((layer) => layer["id"]))
if (dups.length > 0) { if (dups.length > 0) {
context.err( context.err(
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}` `The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`

View file

@ -24,6 +24,7 @@ import { AvailableRasterLayers } from "../../RasterLayers"
import { eliCategory } from "../../RasterLayerProperties" import { eliCategory } from "../../RasterLayerProperties"
import licenses from "../../../assets/generated/license_info.json" import licenses from "../../../assets/generated/license_info.json"
import { Strings } from "../../../Utils/Strings" import { Strings } from "../../../Utils/Strings"
import { Lists } from "../../../Utils/Lists"
export class ValidateLanguageCompleteness extends DesugaringStep<ThemeConfig> { export class ValidateLanguageCompleteness extends DesugaringStep<ThemeConfig> {
private readonly _languages: string[] private readonly _languages: string[]
@ -1064,7 +1065,7 @@ export class DetectDuplicatePresets extends DesugaringStep<ThemeConfig> {
const enNames = presets.map((p) => p.title.textFor("en")) const enNames = presets.map((p) => p.title.textFor("en"))
if (new Set(enNames).size != enNames.length) { if (new Set(enNames).size != enNames.length) {
const dups = Utils.Duplicates(enNames) const dups = Lists.duplicates(enNames)
const layersWithDup = json.layers.filter((l) => const layersWithDup = json.layers.filter((l) =>
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0) l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0)
) )

View file

@ -13,7 +13,6 @@ import PointRenderingConfig from "./PointRenderingConfig"
import WithContextLoader from "./WithContextLoader" import WithContextLoader from "./WithContextLoader"
import LineRenderingConfig from "./LineRenderingConfig" import LineRenderingConfig from "./LineRenderingConfig"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import { Utils } from "../../Utils"
import { TagsFilter } from "../../Logic/Tags/TagsFilter" import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import FilterConfigJson from "./Json/FilterConfigJson" import FilterConfigJson from "./Json/FilterConfigJson"
import { Overpass } from "../../Logic/Osm/Overpass" import { Overpass } from "../../Logic/Osm/Overpass"
@ -340,7 +339,7 @@ export default class LayerConfig extends WithContextLoader {
} }
{ {
const duplicateIds = Utils.Duplicates(this.filters.map((f) => f.id)) const duplicateIds = Lists.duplicates(this.filters.map((f) => f.id))
if (duplicateIds.length > 0) { if (duplicateIds.length > 0) {
throw `Some filters have a duplicate id: ${duplicateIds} (at ${context}.filters)` throw `Some filters have a duplicate id: ${duplicateIds} (at ${context}.filters)`
} }

View file

@ -23,6 +23,7 @@
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { default as Trans } from "../Base/Tr.svelte" import { default as Trans } from "../Base/Tr.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte" import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import { Lists } from "../../Utils/Lists"
export let state: ThemeViewState & SpecialVisualizationState = undefined export let state: ThemeViewState & SpecialVisualizationState = undefined
export let autoDownload = state.autoDownloadOfflineBasemap export let autoDownload = state.autoDownloadOfflineBasemap
@ -202,7 +203,7 @@
<AccordionItem paddingDefault="p-2"> <AccordionItem paddingDefault="p-2">
<Trans t={t.overview} slot="header" /> <Trans t={t.overview} slot="header" />
<div class="leave-room"> <div class="leave-room">
{Utils.toHumanByteSize(Utils.sum($installed.map((area) => area.size)))} {Utils.toHumanByteSize(Lists.sum($installed.map((area) => area.size)))}
<button <button
on:click={() => { on:click={() => {
installed?.data?.forEach((area) => del(area)) installed?.data?.forEach((area) => del(area))

View file

@ -8,6 +8,7 @@
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Lists } from "../../Utils/Lists"
export let search: UIEventSource<string> = new UIEventSource<string>(undefined) export let search: UIEventSource<string> = new UIEventSource<string>(undefined)
export let themes: MinimalThemeInformation[] export let themes: MinimalThemeInformation[]
@ -23,7 +24,7 @@
? "flex flex-wrap items-center justify-center gap-x-2" ? "flex flex-wrap items-center justify-center gap-x-2"
: "theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3"} : "theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3"}
> >
{#each Utils.DedupOnId(Utils.noNull(themes)) as theme (theme.id)} {#each Lists.dedupOnId(Utils.noNull(themes)) as theme (theme.id)}
<ThemeButton {theme} {state} iconOnly={onlyIcons}> <ThemeButton {theme} {state} iconOnly={onlyIcons}>
{#if $search && hasSelection && themes?.[0] === theme} {#if $search && hasSelection && themes?.[0] === theme}
<span class="thanks hidden-on-mobile" aria-hidden="true"> <span class="thanks hidden-on-mobile" aria-hidden="true">

View file

@ -9,10 +9,9 @@
import OpeningHoursRangeElement from "./OpeningHoursRangeElement.svelte" import OpeningHoursRangeElement from "./OpeningHoursRangeElement.svelte"
import { Translation } from "../../i18n/Translation" import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations" import Translations from "../../i18n/Translations"
import { OH } from "../OpeningHours"
import type { OpeningRange } from "../OpeningHours" import type { OpeningRange } from "../OpeningHours"
import { OH } from "../OpeningHours"
import { Utils } from "../../../Utils" import { Lists } from "../../../Utils/Lists"
export let oh: opening_hours export let oh: opening_hours
export let ranges: OpeningRange[][] // Per weekday export let ranges: OpeningRange[][] // Per weekday
@ -47,7 +46,7 @@
changeTexts: string[] changeTexts: string[]
}[] = OH.partitionOHForDistance(changeHoursWeekend, changeHourTextWeekend) }[] = OH.partitionOHForDistance(changeHoursWeekend, changeHourTextWeekend)
let allChangeMoments: number[] = Utils.DedupT([...changeHours, ...changeHoursWeekend]) let allChangeMoments: number[] = Lists.dedupT([...changeHours, ...changeHoursWeekend])
let todayChangeMoments: Set<number> = new Set(OH.allChangeMoments(todayRanges)[0]) let todayChangeMoments: Set<number> = new Set(OH.allChangeMoments(todayRanges)[0])
// By default, we always show the range between 8 - 19h, in order to give a stable impression // By default, we always show the range between 8 - 19h, in order to give a stable impression
// Ofc, a bigger range is used if needed // Ofc, a bigger range is used if needed

View file

@ -5,10 +5,12 @@
import { TrashIcon } from "@babeard/svelte-heroicons/mini" import { TrashIcon } from "@babeard/svelte-heroicons/mini"
import ShowConversionMessage from "./ShowConversionMessage.svelte" import ShowConversionMessage from "./ShowConversionMessage.svelte"
import Markdown from "../Base/Markdown.svelte" import Markdown from "../Base/Markdown.svelte"
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" import type {
QuestionableTagRenderingConfigJson
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import CollapsedTagRenderingPreview from "./CollapsedTagRenderingPreview.svelte" import CollapsedTagRenderingPreview from "./CollapsedTagRenderingPreview.svelte"
import { Accordion } from "flowbite-svelte" import { Accordion } from "flowbite-svelte"
import { Utils } from "../../Utils" import { Lists } from "../../Utils/Lists"
export let state: EditJsonState<any> export let state: EditJsonState<any>
export let path: (string | number)[] = [] export let path: (string | number)[] = []
@ -39,7 +41,7 @@
let datapath = path let datapath = path
const currentValue = state.getStoreFor<(string | QuestionableTagRenderingConfigJson)[]>(datapath) const currentValue = state.getStoreFor<(string | QuestionableTagRenderingConfigJson)[]>(datapath)
currentValue.set(Utils.DedupT(currentValue.data)) currentValue.set(Lists.dedupT(currentValue.data))
console.log("Current value is", currentValue.data) console.log("Current value is", currentValue.data)
if (currentValue.data === undefined) { if (currentValue.data === undefined) {
currentValue.setData([]) currentValue.setData([])

View file

@ -252,99 +252,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
return str return str
} }
public static DedupT<T>(arr: T[]): T[] {
if (!arr) {
return arr
}
const items = []
for (const item of arr) {
if (items.indexOf(item) < 0) {
items.push(item)
}
}
return items
}
/**
* Deduplicates the given array based on some ID-properties.
* Removes all falsey values
* @param arr
* @param toKey
* @constructor
*/
public static DedupOnId<T = { id: string }>(
arr: T[],
toKey?: (t: T) => string | string[]
): T[] {
const uniq: T[] = []
const seen = new Set<string>()
if (toKey === undefined) {
toKey = (item) => item["id"]
}
for (const img of arr) {
if (!img) {
continue
}
const ks = toKey(img)
if (typeof ks === "string") {
if (!seen.has(ks)) {
seen.add(ks)
uniq.push(img)
}
} else if (ks) {
const ksNoNull = Lists.noNull(ks)
const hasBeenSeen = ksNoNull.some((k) => seen.has(k))
if (!hasBeenSeen) {
uniq.push(img)
}
for (const k of ksNoNull) {
seen.add(k)
}
}
}
return uniq
}
/**
* Finds all duplicates in a list of strings
*
* Utils.Duplicates(["a", "b", "c"]) // => []
* Utils.Duplicates(["a", "b","c","b"] // => ["b"]
* Utils.Duplicates(["a", "b","c","b","b"] // => ["b"]
*
*/
public static Duplicates(arr: string[]): string[] {
if (arr === undefined) {
return undefined
}
const seen = new Set<string>()
const duplicates = new Set<string>()
for (const string of arr) {
if (seen.has(string)) {
duplicates.add(string)
}
seen.add(string)
}
return Array.from(duplicates)
}
/**
* In the given list, all values which are lists will be merged with the values, e.g.
*
* Utils.Flatten([ [1,2], 3, [4, [5 ,6]] ]) // => [1, 2, 3, 4, [5, 6]]
*/
public static Flatten<T>(list: (T | T[])[]): T[] {
const result = []
for (const value of list) {
if (Array.isArray(value)) {
result.push(...value)
} else {
result.push(value)
}
}
return result
}
/** /**
* Utils.Identical([1,2], [1,2]) // => true * Utils.Identical([1,2], [1,2]) // => true
* Utils.Identical([1,2,3], [1,2,4}]) // => false * Utils.Identical([1,2,3], [1,2,4}]) // => false
@ -1787,14 +1694,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return href return href
} }
public static sum(list: number[]): number {
let total = 0
for (const number of list) {
total += number
}
return total
}
/** /**
* *
* JSON.stringify(Utils.reorder({b: "0", a: "1"}, ["a", "b"])) // => '{"a":"1","b":"0"}' * JSON.stringify(Utils.reorder({b: "0", a: "1"}, ["a", "b"])) // => '{"a":"1","b":"0"}'

View file

@ -86,4 +86,93 @@ export class Lists {
} }
return items return items
} }
public static sum(list: number[]): number {
let total = 0
for (const number of list) {
total += number
}
return total
}
/**
* In the given list, all values which are lists will be merged with the values, e.g.
*
* Utils.Flatten([ [1,2], 3, [4, [5 ,6]] ]) // => [1, 2, 3, 4, [5, 6]]
*/
public static flatten<T>(list: (T | T[])[]): T[] {
const result = []
for (const value of list) {
if (Array.isArray(value)) {
result.push(...value)
} else {
result.push(value)
}
}
return result
}
/**
* Deduplicates the given array based on some ID-properties.
* Removes all falsey values
* @param arr
* @param toKey
* @constructor
*/
public static dedupOnId<T = { id: string }>(
arr: T[],
toKey?: (t: T) => string | string[]
): T[] {
const uniq: T[] = []
const seen = new Set<string>()
if (toKey === undefined) {
toKey = (item) => item["id"]
}
for (const img of arr) {
if (!img) {
continue
}
const ks = toKey(img)
if (typeof ks === "string") {
if (!seen.has(ks)) {
seen.add(ks)
uniq.push(img)
}
} else if (ks) {
const ksNoNull = Lists.noNull(ks)
const hasBeenSeen = ksNoNull.some((k) => seen.has(k))
if (!hasBeenSeen) {
uniq.push(img)
}
for (const k of ksNoNull) {
seen.add(k)
}
}
}
return uniq
}
/**
* Finds all duplicates in a list of strings
*
* Utils.Duplicates(["a", "b", "c"]) // => []
* Utils.Duplicates(["a", "b","c","b"] // => ["b"]
* Utils.Duplicates(["a", "b","c","b","b"] // => ["b"]
*
*/
public static duplicates(arr: string[]): string[] {
if (arr === undefined) {
return undefined
}
const seen = new Set<string>()
const duplicates = new Set<string>()
for (const string of arr) {
if (seen.has(string)) {
duplicates.add(string)
}
seen.add(string)
}
return Array.from(duplicates)
}
} }