Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as objectGroupBy from './object-groupby.js'
import * as mapGroupBy from './map-groupby.js'
import * as promiseTry from './promise-try.js'
import * as iteratorHelpers from './iterator-helpers.js'
import * as setMethods from './set-methods.js'

let supportsModalPseudo = false
try {
Expand Down Expand Up @@ -61,6 +62,7 @@ export const polyfills = {
mapGroupBy,
promiseTry,
iteratorHelpers,
setMethods,
}

export function isSupported() {
Expand Down
131 changes: 131 additions & 0 deletions src/set-methods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Set-like interface per TC39 spec: must have has, keys, and size
interface SetLike<T> {
has(value: T): boolean
keys(): IterableIterator<T>
readonly size: number
}

/*#__PURE__*/
export function union<T>(this: Set<T>, other: SetLike<T>): Set<T> {
const result = new Set<T>(this)
for (const value of other.keys()) {
result.add(value)
}
return result
}

/*#__PURE__*/
export function intersection<T>(this: Set<T>, other: SetLike<T>): Set<T> {
const result = new Set<T>()
for (const value of this) {
if (other.has(value)) {
result.add(value)
}
}
return result
}

/*#__PURE__*/
export function difference<T>(this: Set<T>, other: SetLike<T>): Set<T> {
const result = new Set<T>()
for (const value of this) {
if (!other.has(value)) {
result.add(value)
}
}
return result
}

/*#__PURE__*/
export function symmetricDifference<T>(this: Set<T>, other: SetLike<T>): Set<T> {
const result = new Set<T>(this)
for (const value of other.keys()) {
if (result.has(value)) {
result.delete(value)
} else {
result.add(value)
}
}
return result
}

/*#__PURE__*/
export function isSubsetOf<T>(this: Set<T>, other: SetLike<T>): boolean {
if (this.size > other.size) return false
for (const value of this) {
if (!other.has(value)) {
return false
}
}
return true
}

/*#__PURE__*/
export function isSupersetOf<T>(this: Set<T>, other: SetLike<T>): boolean {
for (const value of other.keys()) {
if (!this.has(value)) {
return false
}
}
return true
}

/*#__PURE__*/
export function isDisjointFrom<T>(this: Set<T>, other: SetLike<T>): boolean {
if (this.size <= other.size) {
for (const value of this) {
if (other.has(value)) {
return false
}
}
} else {
for (const value of other.keys()) {
if (this.has(value)) {
return false
}
}
}
return true
}

/*#__PURE__*/
export function isSupported(): boolean {
return (
'union' in Set.prototype &&
'intersection' in Set.prototype &&
'difference' in Set.prototype &&
'symmetricDifference' in Set.prototype &&
'isSubsetOf' in Set.prototype &&
'isSupersetOf' in Set.prototype &&
'isDisjointFrom' in Set.prototype
)
}

/*#__PURE__*/
export function isPolyfilled(): boolean {
const proto = Set.prototype as unknown as Record<string, unknown>
return (
'union' in Set.prototype &&
proto['union'] === union &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not adding this check: proto['union'] === union && to isSupported. Is that intended?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, I believe that's right, if I understand it correctly?

isSupported supports both polyfilled and not, so it should cover both cases (once the polyfill is applied), while isPolyfilled validates whether the support came from the polyfill applied or by built in support (which I think for us is everything but safari 16 or 17)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, yeah that makes sense!

proto['intersection'] === intersection &&
proto['difference'] === difference &&
proto['symmetricDifference'] === symmetricDifference &&
proto['isSubsetOf'] === isSubsetOf &&
proto['isSupersetOf'] === isSupersetOf &&
proto['isDisjointFrom'] === isDisjointFrom
)
}

export function apply(): void {
if (!isSupported()) {
Object.assign(Set.prototype, {
union,
intersection,
difference,
symmetricDifference,
isSubsetOf,
isSupersetOf,
isDisjointFrom,
})
}
}
148 changes: 148 additions & 0 deletions test/set-methods.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {expect} from 'chai'
import {
apply,
isPolyfilled,
isSupported,
union,
intersection,
difference,
symmetricDifference,
isSubsetOf,
isSupersetOf,
isDisjointFrom,
} from '../src/set-methods.ts'

// eslint-disable-next-line i18n-text/no-en
describe('Set methods', () => {
it('has standard isSupported, isPolyfilled, apply API', () => {
expect(isSupported).to.be.a('function')
expect(isPolyfilled).to.be.a('function')
expect(apply).to.be.a('function')
expect(isSupported()).to.be.a('boolean')
expect(isPolyfilled()).to.equal(false)
})

describe('union', () => {
it('returns a new Set with elements from both sets', () => {
const a = new Set([1, 2, 3])
const b = new Set([3, 4, 5])
const result = union.call(a, b)
expect(result).to.be.instanceof(Set)
expect([...result].sort()).to.eql([1, 2, 3, 4, 5])
})

it('handles empty sets', () => {
const a = new Set([1, 2])
const result = union.call(a, new Set())
expect([...result].sort()).to.eql([1, 2])
})
})

describe('intersection', () => {
it('returns a new Set with elements in both sets', () => {
const a = new Set([1, 2, 3])
const b = new Set([2, 3, 4])
const result = intersection.call(a, b)
expect(result).to.be.instanceof(Set)
expect([...result].sort()).to.eql([2, 3])
})

it('returns empty set when no common elements', () => {
const a = new Set([1, 2])
const b = new Set([3, 4])
const result = intersection.call(a, b)
expect(result.size).to.equal(0)
})
})

describe('difference', () => {
it('returns a new Set with elements in this but not other', () => {
const a = new Set([1, 2, 3])
const b = new Set([2, 3, 4])
const result = difference.call(a, b)
expect(result).to.be.instanceof(Set)
expect([...result]).to.eql([1])
})

it('returns a copy of this when no overlap', () => {
const a = new Set([1, 2])
const b = new Set([3, 4])
const result = difference.call(a, b)
expect([...result].sort()).to.eql([1, 2])
})
})

describe('symmetricDifference', () => {
it('returns elements in one set but not both', () => {
const a = new Set([1, 2, 3])
const b = new Set([2, 3, 4])
const result = symmetricDifference.call(a, b)
expect(result).to.be.instanceof(Set)
expect([...result].sort()).to.eql([1, 4])
})

it('returns empty set for identical sets', () => {
const a = new Set([1, 2])
const result = symmetricDifference.call(a, new Set([1, 2]))
expect(result.size).to.equal(0)
})
})

describe('isSubsetOf', () => {
it('returns true when all elements are in the other set', () => {
const a = new Set([1, 2])
const b = new Set([1, 2, 3])
expect(isSubsetOf.call(a, b)).to.equal(true)
})

it('returns false when some elements are not in the other set', () => {
const a = new Set([1, 2, 4])
const b = new Set([1, 2, 3])
expect(isSubsetOf.call(a, b)).to.equal(false)
})

it('returns true for empty set', () => {
const a = new Set()
const b = new Set([1, 2])
expect(isSubsetOf.call(a, b)).to.equal(true)
})
})

describe('isSupersetOf', () => {
it('returns true when this set contains all elements of other', () => {
const a = new Set([1, 2, 3])
const b = new Set([1, 2])
expect(isSupersetOf.call(a, b)).to.equal(true)
})

it('returns false when other has elements not in this', () => {
const a = new Set([1, 2])
const b = new Set([1, 2, 3])
expect(isSupersetOf.call(a, b)).to.equal(false)
})

it('returns true when other is empty', () => {
const a = new Set([1, 2])
expect(isSupersetOf.call(a, new Set())).to.equal(true)
})
})

describe('isDisjointFrom', () => {
it('returns true when sets have no common elements', () => {
const a = new Set([1, 2])
const b = new Set([3, 4])
expect(isDisjointFrom.call(a, b)).to.equal(true)
})

it('returns false when sets share elements', () => {
const a = new Set([1, 2, 3])
const b = new Set([3, 4])
expect(isDisjointFrom.call(a, b)).to.equal(false)
})

it('returns true when either set is empty', () => {
const a = new Set([1, 2])
expect(isDisjointFrom.call(a, new Set())).to.equal(true)
})
})
})