forked from MapComplete/MapComplete
Refactoring: move functions out of Utils
This commit is contained in:
parent
442f5a2923
commit
5ea2414204
16 changed files with 126 additions and 145 deletions
|
@ -22,7 +22,6 @@ import ThemeViewState from "../src/Models/ThemeViewState"
|
|||
import Validators from "../src/UI/InputElement/Validators"
|
||||
import questions from "../public/assets/generated/layers/questions.json"
|
||||
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import { Utils } from "../src/Utils"
|
||||
import { TagUtils } from "../src/Logic/Tags/TagUtils"
|
||||
import Script from "./Script"
|
||||
import { Changes } from "../src/Logic/Osm/Changes"
|
||||
|
@ -562,7 +561,7 @@ export class GenerateDocs extends Script {
|
|||
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)))
|
||||
titles.sort()
|
||||
|
||||
|
|
|
@ -327,7 +327,7 @@ export class GenerateLicenseInfo extends Script {
|
|||
}
|
||||
|
||||
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"
|
||||
if (licenses.length === 0) {
|
||||
console.log("Removing", path, "as it is empty")
|
||||
|
|
|
@ -6,7 +6,6 @@ import { ImmutableStore, Store, Stores } from "../UIEventSource"
|
|||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||
import { WikidataImageProvider } from "./WikidataImageProvider"
|
||||
import Panoramax from "./Panoramax"
|
||||
import { Utils } from "../../Utils"
|
||||
import { ServerSourceInfo } from "../../Models/SourceOverview"
|
||||
import { Lists } from "../../Utils/Lists"
|
||||
|
||||
|
@ -151,7 +150,7 @@ export default class AllImageProviders {
|
|||
}
|
||||
const source = Stores.concat(allSources).map((result) => {
|
||||
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
|
||||
return source
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Point } from "geojson"
|
|||
import { ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist"
|
||||
import { Mapillary } from "../ImageProviders/Mapillary"
|
||||
import { ServerSourceInfo } from "../../Models/SourceOverview"
|
||||
import { Lists } from "../../Utils/Lists"
|
||||
|
||||
interface ImageFetcher {
|
||||
/**
|
||||
|
@ -465,6 +466,6 @@ export class CombinedFetcher {
|
|||
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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ import ThemeConfig from "../../src/Models/ThemeConfig/ThemeConfig"
|
|||
import { ThemeConfigJson } from "../../src/Models/ThemeConfig/Json/ThemeConfigJson"
|
||||
import SpecialVisualizations from "../../src/UI/SpecialVisualizations"
|
||||
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 { Lists } from "../Utils/Lists"
|
||||
|
||||
|
@ -206,7 +208,7 @@ export class SourceOverview {
|
|||
}
|
||||
urls = urls.filter((item) => !!item?.url)
|
||||
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
|
||||
return urls
|
||||
}
|
||||
|
|
|
@ -1,18 +1,6 @@
|
|||
import {
|
||||
Concat,
|
||||
DesugaringContext,
|
||||
DesugaringStep,
|
||||
Each,
|
||||
FirstOf,
|
||||
Fuse,
|
||||
On,
|
||||
SetDefault,
|
||||
} from "./Conversion"
|
||||
import { Concat, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault } from "./Conversion"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import {
|
||||
MinimalTagRenderingConfigJson,
|
||||
TagRenderingConfigJson,
|
||||
} from "../Json/TagRenderingConfigJson"
|
||||
import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import RewritableConfigJson from "../Json/RewritableConfigJson"
|
||||
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
|
||||
|
@ -1163,7 +1151,9 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
"Fully prepares and expands a layer for the LayerConfig.",
|
||||
new DeriveSource(),
|
||||
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(
|
||||
"tagRenderings",
|
||||
(layer) =>
|
||||
|
@ -1180,7 +1170,9 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
new On<
|
||||
(LineRenderingConfigJson | RewritableConfigJson<LineRenderingConfigJson>)[],
|
||||
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>(
|
||||
"pointRendering",
|
||||
(layer) =>
|
||||
|
|
|
@ -178,9 +178,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
|
||||
{
|
||||
// Check for multiple, identical builtin questions - usability for studio users
|
||||
const duplicates = Utils.Duplicates(
|
||||
<string[]>json.tagRenderings.filter((tr) => typeof tr === "string")
|
||||
)
|
||||
const duplicates = Lists.duplicates(<string[]>json.tagRenderings.filter((tr) => typeof tr === "string"))
|
||||
for (let i = 0; i < json.tagRenderings.length; i++) {
|
||||
const tagRendering = json.tagRenderings[i]
|
||||
if (typeof tagRendering === "string" && duplicates.indexOf(tagRendering) > 0) {
|
||||
|
@ -209,7 +207,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
{
|
||||
// duplicate ids in tagrenderings check
|
||||
const duplicates = Lists.noNull(
|
||||
Utils.Duplicates(json.tagRenderings?.map((tr) => tr?.["id"]))
|
||||
Lists.duplicates(json.tagRenderings?.map((tr) => tr?.["id"]))
|
||||
)
|
||||
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
|
||||
|
@ -312,9 +310,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
)
|
||||
}
|
||||
|
||||
const duplicateIds = Utils.Duplicates(
|
||||
(json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions")
|
||||
)
|
||||
const duplicateIds = Lists.duplicates((json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions"))
|
||||
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
|
||||
context
|
||||
.enter("tagRenderings")
|
||||
|
|
|
@ -7,6 +7,7 @@ import ThemeConfig from "../ThemeConfig"
|
|||
import { Utils } from "../../../Utils"
|
||||
import { DetectDuplicatePresets, DoesImageExist, ValidateLanguageCompleteness } from "./Validation"
|
||||
import Constants from "../../Constants"
|
||||
import { Lists } from "../../../Utils/Lists"
|
||||
|
||||
export class ValidateTheme extends DesugaringStep<ThemeConfigJson> {
|
||||
/**
|
||||
|
@ -105,7 +106,7 @@ export class ValidateTheme extends DesugaringStep<ThemeConfigJson> {
|
|||
}
|
||||
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) {
|
||||
context.err(
|
||||
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`
|
||||
|
|
|
@ -24,6 +24,7 @@ import { AvailableRasterLayers } from "../../RasterLayers"
|
|||
import { eliCategory } from "../../RasterLayerProperties"
|
||||
import licenses from "../../../assets/generated/license_info.json"
|
||||
import { Strings } from "../../../Utils/Strings"
|
||||
import { Lists } from "../../../Utils/Lists"
|
||||
|
||||
export class ValidateLanguageCompleteness extends DesugaringStep<ThemeConfig> {
|
||||
private readonly _languages: string[]
|
||||
|
@ -1064,7 +1065,7 @@ export class DetectDuplicatePresets extends DesugaringStep<ThemeConfig> {
|
|||
|
||||
const enNames = presets.map((p) => p.title.textFor("en"))
|
||||
if (new Set(enNames).size != enNames.length) {
|
||||
const dups = Utils.Duplicates(enNames)
|
||||
const dups = Lists.duplicates(enNames)
|
||||
const layersWithDup = json.layers.filter((l) =>
|
||||
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0)
|
||||
)
|
||||
|
|
|
@ -13,7 +13,6 @@ import PointRenderingConfig from "./PointRenderingConfig"
|
|||
import WithContextLoader from "./WithContextLoader"
|
||||
import LineRenderingConfig from "./LineRenderingConfig"
|
||||
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
|
||||
import { Utils } from "../../Utils"
|
||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||
import FilterConfigJson from "./Json/FilterConfigJson"
|
||||
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) {
|
||||
throw `Some filters have a duplicate id: ${duplicateIds} (at ${context}.filters)`
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import { default as Trans } from "../Base/Tr.svelte"
|
||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||
import { Lists } from "../../Utils/Lists"
|
||||
|
||||
export let state: ThemeViewState & SpecialVisualizationState = undefined
|
||||
export let autoDownload = state.autoDownloadOfflineBasemap
|
||||
|
@ -202,7 +203,7 @@
|
|||
<AccordionItem paddingDefault="p-2">
|
||||
<Trans t={t.overview} slot="header" />
|
||||
<div class="leave-room">
|
||||
{Utils.toHumanByteSize(Utils.sum($installed.map((area) => area.size)))}
|
||||
{Utils.toHumanByteSize(Lists.sum($installed.map((area) => area.size)))}
|
||||
<button
|
||||
on:click={() => {
|
||||
installed?.data?.forEach((area) => del(area))
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Lists } from "../../Utils/Lists"
|
||||
|
||||
export let search: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
export let themes: MinimalThemeInformation[]
|
||||
|
@ -23,7 +24,7 @@
|
|||
? "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"}
|
||||
>
|
||||
{#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}>
|
||||
{#if $search && hasSelection && themes?.[0] === theme}
|
||||
<span class="thanks hidden-on-mobile" aria-hidden="true">
|
||||
|
|
|
@ -9,10 +9,9 @@
|
|||
import OpeningHoursRangeElement from "./OpeningHoursRangeElement.svelte"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { OH } from "../OpeningHours"
|
||||
import type { OpeningRange } from "../OpeningHours"
|
||||
|
||||
import { Utils } from "../../../Utils"
|
||||
import { OH } from "../OpeningHours"
|
||||
import { Lists } from "../../../Utils/Lists"
|
||||
|
||||
export let oh: opening_hours
|
||||
export let ranges: OpeningRange[][] // Per weekday
|
||||
|
@ -47,7 +46,7 @@
|
|||
changeTexts: string[]
|
||||
}[] = 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])
|
||||
// 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
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import ShowConversionMessage from "./ShowConversionMessage.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 { Accordion } from "flowbite-svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Lists } from "../../Utils/Lists"
|
||||
|
||||
export let state: EditJsonState<any>
|
||||
export let path: (string | number)[] = []
|
||||
|
@ -39,7 +41,7 @@
|
|||
|
||||
let datapath = path
|
||||
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)
|
||||
if (currentValue.data === undefined) {
|
||||
currentValue.setData([])
|
||||
|
|
101
src/Utils.ts
101
src/Utils.ts
|
@ -252,99 +252,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
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,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
|
||||
}
|
||||
|
||||
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"}'
|
||||
|
|
|
@ -86,4 +86,93 @@ export class Lists {
|
|||
}
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue