Hash PixelAspectRatio by the ratio it compares - #331
Merged
Merged
Conversation
PixelAspectRatio compares by height / width, so 4:3, 8:6 and a 4032x3024 image size are equal, but its synthesized hash(into:) combined the stored width and height. Equal values could hash differently, so a Set could keep duplicate ratios and a Dictionary keyed by 4:3 could miss 4032x3024. Hash the same proportion that == compares. Ratio equality stays: crop re-fitting and the PhotosCrop picker rely on 4032x3024 matching 4:3. `id` stays the exact width/height pair so ForEach rows over presets that share a proportion remain distinct; both contracts are now documented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A single lookup can hit an equal key by chance under the per-process hash seed, so the dictionary test now also looks up 4:3 and 16:9 scaled by 1...100. A new test pins the exact-proportion edge cases: +0 and -0, infinite proportions from a zero width, and NaN from zero by zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PixelAspectRatiodefines==on the proportion (height / width), so4:3,8:6and a4032x3024image size are equal. ItsHashableconformance, however, was still synthesized, which hashes the storedwidthandheight. Equal values could therefore produce different hash values, which breaks theHashablecontract:Set([4:3, 8:6])could keep both elements, depending on bucket placement.Dictionarykeyed by4:3could miss a lookup with4032x3024.This PR gives
PixelAspectRatioan explicithash(into:)that hashes the same proportion==compares.Who is affected
No code in the library hashes
PixelAspectRatio: the types that store one (CropView.State,SwiftUICropView.StateSnapshot,CropEditingState,PhotosCropAspectRatioSelection,SwiftUIPhotosCropView.AspectRatioOptions) are onlyEquatableor have no conformance, and the oneForEachusesid. The fix matters for host apps that putPixelAspectRatioin aSet, use it as aDictionarykey, or useForEach(id: \.self). There, equal ratios such as16:9and1920x1080now collapse into one element, as==already said.Geometry.swiftis identical onmain,v5and the v6 branch, so the same bug is still there until this is merged forward. The v6 branch has not touched the file, so mergingmaininto it after this lands should be clean.Equality semantics are unchanged
I kept ratio equality because every caller in the library relies on it:
CropView.setCroppingAspectRatio(_:)andCropEditingState.updateCropExtentIfNeeded(toFitAspectRatio:)skip re-fitting the crop when the new ratio equals the current one. A4032x3024original and a4:3preset describe the same crop shape, so they should not re-fit.PhotosCropAspectRatioSelection.sync(aspectRatio:originalAspectRatio:)recognizes that a ratio coming back from the crop view is the image's original ratio (or its swapped form) by comparing with==.selection.isRatio(_:)against minimized presets, and checks!= .square.Structural equality would make these checks fail for equivalent ratios that are written with different numbers, so the fix is on the hashing side.
idstays structuralidis left as the exact width/height pair (it was already"\(width.bitPattern), \(height.bitPattern)"). Its only use isForEach(Self.horizontalRectangleAspectRatios)in the PhotosCrop picker.Identifiableidentity does not have to match==, and a structural id keeps presets that share a proportion (for example5:4and10:8) as separate, stable rows. Keying rows by ratio would make such presets collide inForEach. The id is now documented as finer than==.Notes
a ≈ bandb ≈ cdo not givea ≈ c), so no hash could agree with it. Equal proportions written with exact numbers (integers, or values like1.5:1vs3:2) divide to the same correctly roundedCGFloat, so they hash equally. Inexact decimal inputs can differ:0.3:0.1is not equal to3:1. This is unchanged from before.height / width, which gives these edge cases (all covered by a test):+0or-0(for4:0and-4:0). They are equal, andDoublehashing already treats them as the same value.0:3and0:7are equal and hash equally.-0:3is negative infinity and is not equal to0:3.localizedTextstill prints the authored numbers, so two equal values can render different text (4:3vs8:6). Callers that need a canonical label already use_minimized().Tests
New
Dev/Tests/BrightroomEngineTests/PixelAspectRatioTests.swift. Withmain'sGeometry.swift, 4 of its 6 tests fail (hash values,Setsize,Dictionarylookup, zero sides); the other two check behavior this PR keeps.4:3scaled by 1...100 has the same hash value as4:3.Setof 100 scaled16:9values holds one element, or two when the swapped values are added.Dictionarykeyed by4:3/16:9finds entries through4032x3024/1920x1080, and through4:3and16:9scaled by 1...100. Swift seeds hashing per process, so with the old hash a single lookup can still hit the equal key by chance; the 200 scaled lookups make the test fail reliably onmain(200 of 200 runs of a standalone copy of the old code failed it).4:0and-4:0are equal with equal hash values,0:3and0:7likewise,-0:3differs from0:3, and0:0is not equal to itself.iddiffers for5:4and10:8, and is stable for the same pair.Verification:
PixelAspectRatioTestswithmain'sGeometry.swift: 4 of 6 failed, as expectedBrightroomParametricTests: passed🤖 Generated with Claude Code