Compare commits

...

4 Commits

Author SHA1 Message Date
Davy Hélard
645a6ce900 Use AutoSizer 2022-06-02 19:04:03 +02:00
Davy Hélard
22b9e710b9 WIP 2022-05-31 18:52:08 +02:00
Davy Hélard
b585ac3d56 Add tests and comments. 2022-05-30 17:56:54 +02:00
Davy Hélard
3a0a2152d1 Search sort to make the color filter not excluding. 2022-05-28 00:53:01 +02:00
7 changed files with 529 additions and 78 deletions

View File

@@ -32,6 +32,13 @@ import ArrowForwardIos from '@material-ui/icons/ArrowForwardIos';
import ThemeContext from '../UI/Theme/ThemeContext';
import AnimationPreview from '../ObjectEditor/Editors/SpriteEditor/AnimationPreview';
import ScrollView from '../UI/ScrollView';
import { BoxSearchResultsNoScroll } from '../UI/Search/BoxSearchResultsNoScroll';
import { AssetCard } from './AssetCard';
import { NoResultPlaceholder } from './NoResultPlaceholder';
import { useSearchItem, SearchFilter } from '../UI/Search/UseSearchItem';
import {
SimilarAssetStoreSearchFilter,
} from './AssetStoreSearchFilter';
const FIXED_HEIGHT = 250;
@@ -81,6 +88,7 @@ type Props = {|
onClose: () => void,
canInstall: boolean,
isBeingInstalled: boolean,
onOpenDetails: (assetShortHeader: AssetShortHeader) => void,
|};
const getObjectAssetResourcesByName = (
@@ -107,6 +115,7 @@ export const AssetDetails = ({
onClose,
canInstall,
isBeingInstalled,
onOpenDetails,
}: Props) => {
const gdevelopTheme = React.useContext(ThemeContext);
const { authors, licenses } = React.useContext(AssetStoreContext);
@@ -190,7 +199,22 @@ export const AssetDetails = ({
? direction.sprites.map(sprite => assetResources[sprite.image])
: null;
const similarAssetFilters = React.useMemo(() => [new SimilarAssetStoreSearchFilter(assetShortHeader)], [assetShortHeader]);
const {
error: filterError,
fetchAssetsAndFilters,
useSearchItem,
} = React.useContext(AssetStoreContext);
const searchResults = useSearchItem(
"",
null,
null,
similarAssetFilters
);
console.log("searchResults: " + (searchResults && searchResults.length));
return (
<Column expand noMargin useFullHeight>
<ScrollView>
<Column expand noMargin>
<Line justifyContent="space-between" noMargin>
@@ -425,7 +449,36 @@ export const AssetDetails = ({
)}
</Column>
</ResponsiveLineStackLayout>
<Line
expand
overflow={
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
}
>
<Column>
<Text>{"TEST"}</Text>
<Text>{"TEST"}</Text>
<Text>{"TEST"}</Text>
<Text>{"TEST"}</Text>
<Text>{"TEST"}</Text>
</Column>
<BoxSearchResultsNoScroll
baseSize={128}
onRetry={fetchAssetsAndFilters}
error={filterError}
searchItems={searchResults}//{searchResults}
renderSearchItem={(assetShortHeader, size) => (
<AssetCard
size={size}
onOpenDetails={() => onOpenDetails(assetShortHeader)}
assetShortHeader={assetShortHeader}
/>
)}
noResultPlaceholder={<></>}
/>
</Line>
</Column>
</ScrollView>
</Column>
);
};

View File

@@ -20,6 +20,7 @@ import {
LicenseAssetStoreSearchFilter,
DimensionAssetStoreSearchFilter,
} from './AssetStoreSearchFilter';
import { type ChosenCategory } from '../UI/Search/FiltersChooser';
const defaultSearchText = '';
@@ -50,6 +51,12 @@ type AssetStoreState = {|
setSearchText: string => void,
filtersState: FiltersState,
assetFiltersState: AssetFiltersState,
useSearchItem: (
searchText: string,
chosenCategory: ?ChosenCategory,
chosenFilters: Set<string>,
searchFilters: Array<SearchFilter<AssetShortHeader>>
) => ?Array<AssetShortHeader>,
|};
export const AssetStoreContext = React.createContext<AssetStoreState>({
@@ -83,6 +90,12 @@ export const AssetStoreContext = React.createContext<AssetStoreState>({
licenseFilter: new LicenseAssetStoreSearchFilter(),
setLicenseFilter: filter => {},
},
useSearchItem: (
searchText,
chosenCategory,
chosenFilters,
searchFilters,
) => null,
});
type AssetStoreStateProviderProps = {|
@@ -265,6 +278,19 @@ export const AssetStoreStateProvider = ({
licenseFilter: licenseFilter,
setLicenseFilter: setLicenseFilter,
},
useSearchItem: (
searchText,
chosenCategory,
chosenFilters,
searchFilters,
) => useSearchItem(
assetShortHeadersById,
getAssetShortHeaderSearchTerms,
searchText,
chosenCategory,
chosenFilters,
searchFilters
),
}),
[
searchResults,
@@ -288,6 +314,7 @@ export const AssetStoreStateProvider = ({
setColorFilter,
licenseFilter,
setLicenseFilter,
assetShortHeadersById,
]
);

View File

@@ -21,10 +21,11 @@ export class ObjectTypeAssetStoreSearchFilter
this.objectTypes = objectTypes;
}
isSatisfiedBy(searchItem: AssetShortHeader): boolean {
return (
this.objectTypes.size === 0 || this.objectTypes.has(searchItem.objectType)
);
getPertinence(searchItem: AssetShortHeader): number {
return this.objectTypes.size === 0 ||
this.objectTypes.has(searchItem.objectType)
? 1
: 0;
}
}
@@ -43,13 +44,13 @@ export class LicenseAssetStoreSearchFilter
this.attributionFreeOnly = attributionFreeOnly;
}
isSatisfiedBy(searchItem: AssetShortHeader): boolean {
return (
!this.attributionFreeOnly ||
getPertinence(searchItem: AssetShortHeader): number {
return !this.attributionFreeOnly ||
LicenseAssetStoreSearchFilter.noAttributionLicenses.includes(
searchItem.license
)
);
? 1
: 0;
}
}
@@ -66,13 +67,13 @@ export class AnimatedAssetStoreSearchFilter
this.mustHaveSeveralState = mustHaveSeveralState;
}
isSatisfiedBy(searchItem: AssetShortHeader): boolean {
getPertinence(searchItem: AssetShortHeader): number {
const hasAnimatedState = searchItem.maxFramesCount > 1;
const hasSeveralState = searchItem.animationsCount > 1;
return (
(!this.mustBeAnimated || hasAnimatedState) &&
return (!this.mustBeAnimated || hasAnimatedState) &&
(!this.mustHaveSeveralState || hasSeveralState)
);
? 1
: 0;
}
}
@@ -92,17 +93,17 @@ export class DimensionAssetStoreSearchFilter
this.dimensionMax = dimensionMax;
}
isSatisfiedBy(searchItem: AssetShortHeader): boolean {
return (
((this.dimensionMin === DimensionAssetStoreSearchFilter.boundMin ||
this.dimensionMin <= searchItem.width) &&
(this.dimensionMin === DimensionAssetStoreSearchFilter.boundMax ||
searchItem.width <= this.dimensionMax)) ||
getPertinence(searchItem: AssetShortHeader): number {
return ((this.dimensionMin === DimensionAssetStoreSearchFilter.boundMin ||
this.dimensionMin <= searchItem.width) &&
(this.dimensionMin === DimensionAssetStoreSearchFilter.boundMax ||
searchItem.width <= this.dimensionMax)) ||
((this.dimensionMin === DimensionAssetStoreSearchFilter.boundMin ||
this.dimensionMin <= searchItem.height) &&
(this.dimensionMin === DimensionAssetStoreSearchFilter.boundMax ||
searchItem.height <= this.dimensionMax))
);
? 1
: 0;
}
}
@@ -116,6 +117,47 @@ const mod = function(x: number, y: number): number {
return ((x % y) + y) % y;
};
/**
* @param colorA
* @param colorB
* @returns Return the similitude between the 2 colors
* (1 when they are the same).
*/
const getColorSimilitude = function(
colorA: RGBColor,
colorB: RGBColor
): number {
const targetHsl = rgbToHsl(colorA.r, colorA.g, colorA.b);
const dominantHsl = rgbToHsl(colorB.r, colorB.g, colorB.b);
const targetSaturation = targetHsl[1];
const dominantSaturation = dominantHsl[1];
if (targetSaturation === 0) {
// Hue is not relevant.
const deltaSaturation = dominantSaturation - targetSaturation;
const deltaLightness = dominantHsl[2] - targetHsl[2];
return (
1 -
(deltaSaturation * deltaSaturation + deltaLightness * deltaLightness) / 2
);
}
// Hue distance can only be up to 0.5 as it's looping.
// So, it's multiplied by 2 to cover [0, 1].
const deltaHue =
dominantSaturation === 0
? 1
: 2 * Math.abs(mod(dominantHsl[0] - targetHsl[0] + 0.5, 1) - 0.5);
const deltaSaturation = dominantSaturation - targetSaturation;
const deltaLightness = dominantHsl[2] - targetHsl[2];
// Give more importance to hue as it catches human eyes.
return (
1 -
(4 * deltaHue * deltaHue +
deltaSaturation * deltaSaturation +
deltaLightness * deltaLightness) /
6
);
};
export class ColorAssetStoreSearchFilter
implements SearchFilter<AssetShortHeader> {
color: RGBColor | null;
@@ -124,44 +166,108 @@ export class ColorAssetStoreSearchFilter
this.color = color;
}
isSatisfiedBy(searchItem: AssetShortHeader): boolean {
if (!this.color) {
return true;
getPertinence(searchItem: AssetShortHeader): number {
const color = this.color;
if (!color) {
return 1;
}
if (searchItem.dominantColors.length === 0) {
return false;
// Not zero because the item should not be excluded.
let scoreMax = Number.MIN_VALUE;
for (const dominantColor of searchItem.dominantColors) {
const dominantRgb = hexNumberToRGBColor(dominantColor);
const score = getColorSimilitude(dominantRgb, color);
scoreMax = Math.max(scoreMax, score);
}
const targetHsl = rgbToHsl(this.color.r, this.color.g, this.color.b);
const dominantRgb = hexNumberToRGBColor(searchItem.dominantColors[0]);
const dominantHsl = rgbToHsl(dominantRgb.r, dominantRgb.g, dominantRgb.b);
const targetSaturation = targetHsl[1];
const dominantSaturation = dominantHsl[1];
let score = 0;
if (targetSaturation === 0) {
// Hue is not relevant.
const deltaSaturation = dominantSaturation - targetSaturation;
const deltaLightness = dominantHsl[2] - targetHsl[2];
score =
1 -
(deltaSaturation * deltaSaturation + deltaLightness * deltaLightness) /
2;
} else {
// Hue distance can only be up to 0.5 as it's looping.
// So, it's multiplied by 2 to cover [0, 1].
const deltaHue =
dominantSaturation === 0
? 1
: 2 * Math.abs(mod(dominantHsl[0] - targetHsl[0] + 0.5, 1) - 0.5);
const deltaSaturation = dominantSaturation - targetSaturation;
const deltaLightness = dominantHsl[2] - targetHsl[2];
// Give more importance to hue as it catches human eyes.
score =
1 -
(4 * deltaHue * deltaHue +
deltaSaturation * deltaSaturation +
deltaLightness * deltaLightness) /
6;
}
return score > 0.94;
return scoreMax;
}
}
export class SimilarAssetStoreSearchFilter
implements SearchFilter<AssetShortHeader> {
other: AssetShortHeader;
constructor(other: AssetShortHeader) {
this.other = other;
console.log(this.other.tags);
}
getPertinence(searchItem: AssetShortHeader): number {
if (this.other === searchItem ||
this.other.objectType !== searchItem.objectType) {
return 0;
}
{
const hasAnimatedState = searchItem.maxFramesCount > 1;
const hasSeveralState = searchItem.animationsCount > 1;
const mustBeAnimated = this.other.maxFramesCount > 1;
const mustHaveSeveralState = this.other.animationsCount > 1;
const hasBetterAnimationOrState = ((!mustBeAnimated || hasAnimatedState) &&
(!mustHaveSeveralState || hasSeveralState));
if (!hasBetterAnimationOrState) {
return 0;
}
}
{
const isTopDown = searchItem.tags.includes('top-down');
const isIsometric = searchItem.tags.includes('isometric');
const isSideView = searchItem.tags.includes('side view');
const otherIsTopDown = this.other.tags.includes('top-down');
const otherIsIsometric = this.other.tags.includes('isometric');
const otherIsSideView = this.other.tags.includes('side view');
const areCompatible =
(isTopDown && otherIsTopDown) ||
(isIsometric && otherIsIsometric) ||
(isSideView && otherIsSideView) ||
(!isTopDown &&
!isIsometric &&
!isSideView &&
!otherIsTopDown &&
!otherIsIsometric &&
!otherIsSideView);
if (!areCompatible) {
return 0;
}
}
{
const surface = searchItem.width * searchItem.height;
const otherSurface = this.other.width * this.other.height;
const smallestSurface = Math.min(surface, otherSurface);
const greatestSurface = Math.max(surface, otherSurface);
if (2 * smallestSurface < greatestSurface) {
return 0;
}
}
{
const ratio = searchItem.width / searchItem.height;
const otherRatio = this.other.width / this.other.height;
const smallestRatio = Math.min(ratio, otherRatio);
const greatestRatio = Math.max(ratio, otherRatio);
if (1.5 * smallestRatio < greatestRatio) {
return 0;
}
}
let colorSimilitude;
{
let scoreMax = 0;
for (const dominantColor of searchItem.dominantColors) {
const dominantRgb = hexNumberToRGBColor(dominantColor);
for (const otherDominantColor of this.other.dominantColors) {
const otherDominantRgb = hexNumberToRGBColor(otherDominantColor);
const score = getColorSimilitude(dominantRgb, otherDominantRgb);
scoreMax = Math.max(scoreMax, score);
}
}
colorSimilitude = scoreMax;
}
return colorSimilitude;
}
}

View File

@@ -109,6 +109,14 @@ export const AssetStore = ({
const resourcesFetcher = useResourceFetcher();
const onOpenDetails = (assetShortHeader: AssetShortHeader) => {
sendAssetOpened({
id: assetShortHeader.id,
name: assetShortHeader.name,
});
setOpenedAssetShortHeader(assetShortHeader);
};
const onInstallAsset = React.useCallback(
(assetShortHeader: AssetShortHeader) => {
setAssetBeingInstalled(assetShortHeader);
@@ -328,13 +336,7 @@ export const AssetStore = ({
renderSearchItem={(assetShortHeader, size) => (
<AssetCard
size={size}
onOpenDetails={() => {
sendAssetOpened({
id: assetShortHeader.id,
name: assetShortHeader.name,
});
setOpenedAssetShortHeader(assetShortHeader);
}}
onOpenDetails={() => onOpenDetails(assetShortHeader)}
assetShortHeader={assetShortHeader}
/>
)}
@@ -361,6 +363,7 @@ export const AssetStore = ({
!!assetBeingInstalled &&
assetBeingInstalled.id === openedAssetShortHeader.id
}
onOpenDetails={onOpenDetails}
/>
)}
</Line>

View File

@@ -0,0 +1,115 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import PlaceholderLoader from '../PlaceholderLoader';
import PlaceholderError from '../PlaceholderError';
import ErrorBoundary from '../ErrorBoundary';
import { AutoSizer, Grid } from 'react-virtualized';
import EmptyMessage from '../EmptyMessage';
import {
GridListTile,
GridList,
makeStyles,
createStyles,
} from '@material-ui/core';
import Text from '../../UI/Text';
import { Column, Line, Spacer } from '../../UI/Grid';
type Props<SearchItem> = {|
searchItems: ?Array<SearchItem>,
renderSearchItem: (item: SearchItem, size: number) => React.Node,
error: ?Error,
onRetry: () => void,
baseSize: number,
noResultPlaceholder?: React.Node,
|};
const styles = {
container: { flex: 1 },
grid: { overflowX: 'hidden' },
};
export const BoxSearchResultsNoScroll = <SearchItem>({
searchItems,
renderSearchItem,
error,
onRetry,
baseSize,
noResultPlaceholder,
}: Props<SearchItem>) => {
if (!searchItems) {
if (!error) return <PlaceholderLoader />;
else {
return (
<PlaceholderError onRetry={onRetry}>
<Trans>
Can't load the results. Verify your internet connection or retry
later.
</Trans>
</PlaceholderError>
);
}
} else if (searchItems.length === 0) {
return (
noResultPlaceholder || (
<EmptyMessage>
<Trans>
No results returned for your search. Try something else, browse the
categories or create your object from scratch!
</Trans>
</EmptyMessage>
)
);
}
return (
<AutoSizer>
{({ width, height }) => {
if (!width || !height) return null;
console.log("Dimensions: " + width + " " + height);
const columnCount = Math.max(Math.floor((width - 5) / baseSize), 1);
const columnWidth = Math.max(Math.floor(width / columnCount), 30);
const rowCount = Math.max(
1,
Math.ceil(searchItems.length / columnCount)
);
console.log("columnWidth: " + columnWidth);
return (
<GridList
style={styles.grid}
cellHeight={columnWidth}
spacing={0}
cols={columnCount}
style={{
width: width,
height: rowCount * columnWidth,
}}>
>
{searchItems.map((searchItem, index) => {
console.log("Item: " + index);
//return(<Text key={"Similar" + index}>{"ITEMABC"}</Text>);
return (<GridListTile
//classes={classesForGridListItem}
key={"Similar" + index}
cols={1} rows={1}
tabIndex={0}
onKeyPress={(
event: SyntheticKeyboardEvent<HTMLLIElement>
): void => {
// if (shouldValidate(event)) {
// }
}}
onClick={() => {}}
>
{renderSearchItem(searchItem, columnWidth)}
</GridListTile>);
}
)}
</GridList>
);
}}
</AutoSizer>
);
};

View File

@@ -5,7 +5,7 @@ import shuffle from 'lodash/shuffle';
import SearchApi from 'js-worker-search';
export interface SearchFilter<SearchItem> {
isSatisfiedBy(searchItem: SearchItem): boolean;
getPertinence(searchItem: SearchItem): number;
}
export class TagSearchFilter<SearchItem: { tags: Array<string> }>
@@ -16,13 +16,83 @@ export class TagSearchFilter<SearchItem: { tags: Array<string> }>
this.tags = tags;
}
isSatisfiedBy(searchItem: SearchItem): boolean {
return (
this.tags.size === 0 || searchItem.tags.some(tag => this.tags.has(tag))
);
getPertinence(searchItem: SearchItem): number {
return this.tags.size === 0 ||
searchItem.tags.some(tag => this.tags.has(tag))
? 1
: 0;
}
}
/**
* Approximately sort the elements from biggest to smallest.
*
* It does a quick sort but only on the left side. It means that elements with
* a poor pertinence won't be sorted as well as the one with a good pertinence.
*
* This allows a O(n) complexity in most cases, but still a O(n²) in worst
* cases scenario (when all the values are the same).
*/
export const partialQuickSort = <Element: any>(
searchItems: Array<Element>,
getValue: (a: Element) => number,
valueMin: number,
valueMax: number
): void => {
if (valueMin >= valueMax) {
// All values are the same.
return;
}
let indexMax = searchItems.length - 1;
// Values are between 0 and pertinenceMax.
// Each pass ensures that elements are on the good side of the pivot.
// With a pertinenceMax of 1, the pivot takes the values: 0.5, 0.25, 0.125...
for (
let pivotComplement = 0.5;
pivotComplement > 1 / 128 && indexMax > 0;
pivotComplement /= 2
) {
let pivot = valueMin + (valueMax - valueMin) * (1 - pivotComplement);
let slidingIndexMin = 0 - 1;
let slidingIndexMax = indexMax + 1;
while (true) {
do {
slidingIndexMin++;
} while (
slidingIndexMin < indexMax &&
getValue(searchItems[slidingIndexMin]) > pivot
);
if (slidingIndexMin === indexMax) {
// All the values are on the left side.
// They must be sorted.
// Let's try with the next pivot value.
break;
}
do {
slidingIndexMax--;
} while (
slidingIndexMax > 0 &&
getValue(searchItems[slidingIndexMax]) < pivot
);
if (slidingIndexMax === 0) {
// All the values are on the right side.
// As the pivot converge on the maximum value,
// The sort is finished.
return;
}
if (slidingIndexMin >= slidingIndexMax) {
// All values are on the good side of the pivot.
indexMax = slidingIndexMax;
break;
}
const swap = searchItems[slidingIndexMin];
searchItems[slidingIndexMin] = searchItems[slidingIndexMax];
searchItems[slidingIndexMax] = swap;
}
}
};
/**
* Filter a list of items according to the chosen category
* and the chosen filters.
@@ -30,7 +100,7 @@ export class TagSearchFilter<SearchItem: { tags: Array<string> }>
export const filterSearchItems = <SearchItem: { tags: Array<string> }>(
searchItems: ?Array<SearchItem>,
chosenCategory: ?ChosenCategory,
chosenFilters: Set<string>,
chosenFilters: ?Set<string>,
searchFilters?: Array<SearchFilter<SearchItem>>
): ?Array<SearchItem> => {
if (!searchItems) return null;
@@ -62,20 +132,45 @@ export const filterSearchItems = <SearchItem: { tags: Array<string> }>(
})
.filter(searchItem => {
return (
(chosenFilters.size === 0 ||
searchItem.tags.some(tag => chosenFilters.has(tag))) &&
(!searchFilters ||
searchFilters.every(searchFilter =>
searchFilter.isSatisfiedBy(searchItem)
))
!chosenFilters || chosenFilters.size === 0 ||
searchItem.tags.some(tag => chosenFilters.has(tag))
);
});
let sortedSearchItems = filteredSearchItems;
if (searchFilters) {
let pertinenceMin = 1;
let pertinenceMax = 0;
const weightedSearchItems = filteredSearchItems
.map(searchItem => {
let pertinence = 1;
for (const searchFilter of searchFilters) {
pertinence *= searchFilter.getPertinence(searchItem);
if (pertinence === 0) {
return null;
}
}
pertinenceMin = Math.min(pertinenceMin, pertinence);
pertinenceMax = Math.max(pertinenceMax, pertinence);
return { pertinence: pertinence, searchItem: searchItem };
})
.filter(Boolean);
partialQuickSort(
weightedSearchItems,
weightedSearchItem => weightedSearchItem.pertinence,
pertinenceMin,
pertinenceMax
);
sortedSearchItems = weightedSearchItems.map(
weightedSearchItem => weightedSearchItem.searchItem
);
}
const totalTime = performance.now() - startTime;
console.info(
`Filtered items by category/filters in ${totalTime.toFixed(3)}ms.`
);
return filteredSearchItems;
return sortedSearchItems;
};
/**
@@ -93,7 +188,7 @@ export const useSearchItem = <SearchItem: { tags: Array<string> }>(
getItemDescription: SearchItem => string,
searchText: string,
chosenCategory: ?ChosenCategory,
chosenFilters: Set<string>,
chosenFilters: ?Set<string>,
searchFilters?: Array<SearchFilter<SearchItem>>
): ?Array<SearchItem> => {
const searchApiRef = React.useRef<?any>(null);

View File

@@ -1,5 +1,5 @@
// @flow
import { filterSearchItems } from './UseSearchItem';
import { filterSearchItems, partialQuickSort } from './UseSearchItem';
describe('UseSearchItem', () => {
test('sanitizeObjectName', () => {
@@ -15,3 +15,55 @@ describe('UseSearchItem', () => {
]);
});
});
describe('partialQuickSort', () => {
test('Sort an empty search result', () => {
const elements = [];
partialQuickSort(elements, a => a, 1, 0);
expect(elements).toStrictEqual([]);
});
test('Sort an unique search result', () => {
const elements = [1];
partialQuickSort(elements, a => a, 1, 1);
expect(elements).toStrictEqual([1]);
});
[0.4, 0.6, 1].forEach(pertinence => {
test(`Sort search results with same pertinence: ${pertinence}`, () => {
const elements = [];
elements.length = 8;
elements.fill(pertinence);
partialQuickSort(elements, a => a, pertinence, pertinence);
const expectedElements = [];
expectedElements.length = 8;
expectedElements.fill(pertinence);
expect(elements).toStrictEqual(expectedElements);
});
});
test('Sort search results', () => {
const elements = [0.2, 0.1, 0.6, 0.4, 1, 0.3, 0.8, 0.5, 0.7, 0.9];
partialQuickSort(elements, a => a, 0.1, 1);
expect(elements).toStrictEqual(
//
[1, 0.9, 0.8, 0.6, 0.7, 0.3, 0.4, 0.5, 0.1, 0.2]
);
});
test('Sort search results with a lot of small values', () => {
const elements = [0.2, 0.1, 0.15, 0.4, 1, 0.3, 0.25, 0.45, 0.35];
partialQuickSort(elements, a => a, 0.1, 1);
expect(elements).toStrictEqual(
//
[1, 0.1, 0.15, 0.4, 0.2, 0.3, 0.25, 0.45, 0.35]
);
});
test('Sort search results with duplicated values', () => {
const elements = [0.2, 0.6, 0.6, 0.4, 1, 0.8, 0.2, 0.4, 0.8];
partialQuickSort(elements, a => a, 0.1, 1);
expect(elements).toStrictEqual([1, 0.8, 0.8, 0.6, 0.6, 0.4, 0.2, 0.4, 0.2]);
});
test('Sort search results with duplicated maximum values', () => {
const elements = [1, 1, 1, 1, 1, 0.2, 1, 1, 1];
partialQuickSort(elements, a => a, 0.1, 1);
expect(elements).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1, 0.2]);
});
});