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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,6 @@ userscripts/topp-alarm/bell.png
userscripts/topp-alarm/alert.png
.specstory/
python/put_files_into_folder_by_extension/mock_dir/
node_modules/
package-lock.json
package.json
51 changes: 51 additions & 0 deletions parse_test_v2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const parsePrice = str => {
if (!str) return 0;

// Replace terminal dashes with .00 (e.g. 12.- -> 12.00)
let clean = str.replace(/[.–\-]\s*$/g, '.00');

// Remove spaces and apostrophes (always grouping separators) and extract valid chars
clean = clean.replace(/[^\d,.]/g, '').replace(/['’\s]/g, '');

const lastComma = clean.lastIndexOf(',');
const lastDot = clean.lastIndexOf('.');

const lastSeparator = Math.max(lastComma, lastDot);

if (lastSeparator === -1) {
return parseFloat(clean) || 0;
}

const digitsAfterSeparator = clean.length - lastSeparator - 1;

if (digitsAfterSeparator === 3) {
// It's a grouping separator (e.g., 1,385 or 1.385.900)
clean = clean.replace(/[.,]/g, '');
} else {
// It's a decimal separator. Remove all separators before it, replace the last one with '.'
const before = clean.substring(0, lastSeparator).replace(/[.,]/g, '');
const after = clean.substring(lastSeparator + 1);
clean = before + '.' + after;
}

return parseFloat(clean) || 0;
};

const tests = [
["1.385.90", 1385.90],
["1,385.90", 1385.90],
["1.385,90", 1385.90],
["1,385,900", 1385900],
["1.385.900", 1385900],
["1,385", 1385],
["1.385", 1385],
["1'385.90", 1385.90],
["CHF 1'433.00", 1433],
["12.-", 12],
["Gratis", 0]
];

tests.forEach(([input, expected]) => {
const parsed = parsePrice(input);
console.log(`Input: ${input.padEnd(15)} | Parsed: ${parsed} | Expected: ${expected} | ${parsed === expected ? 'PASS' : 'FAIL'}`);
});
37 changes: 36 additions & 1 deletion userscripts/toppreise/tests/test_userscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -1636,6 +1636,34 @@ def test_bestpreise_settings_weight_slider(page: Page):
assert page.evaluate("() => window.ToppreiseSuite.CONFIG.BESTPREISE_WEIGHT_RECORD") == 0.70


def test_parse_price_normalization(page: Page):
"""
Validates that the parsePrice function correctly handles varied European and
international grouping and decimal separator conventions based on the
digits after the final separator.
"""
page.evaluate("""() => {
window.parsePrice = window.ToppreiseSuite.parsePrice;
}""")

test_cases = [
("1.385.90", 1385.90),
("1,385.90", 1385.90),
("1.385,90", 1385.90),
("1,385,900", 1385900),
("1.385.900", 1385900),
("1,385", 1385),
("1'385.90", 1385.90),
("CHF 1'433.00", 1433),
("12.-", 12),
("Gratis", 0)
]

for input_str, expected in test_cases:
safe_input = input_str.replace("'", "\\'")
result = page.evaluate(f"() => window.parsePrice('{safe_input}')")
assert result == expected, f"Expected parsePrice('{input_str}') to be {expected}, but got {result}"

def test_outlier_spike_rejection(page: Page):
# Product: Smartphone normal price ~CHF 1200
# Vendor glitch: 1-day CHF 15 spike on Day 3
Expand Down Expand Up @@ -2378,7 +2406,14 @@ def test_deal_score_weight_preset_dropdown_in_filter_bar(page: Page):
assert '100% Rek' in page.locator('#tp-bar-weight-btn').inner_text()

# Select 100% Median
page.locator('#tp-bar-weight-btn').click()
popover = page.locator('#tp-weight-popover')

# If the popover is not visible, click the button to show it
if not popover.is_visible():
page.locator('#tp-bar-weight-btn').click()

# Explicitly wait for it to be visible based on state, no timeouts or force
popover.wait_for(state="visible")
page.locator('#tp-weight-popover button[data-weight="0.00"]').click()
assert page.evaluate("() => window.ToppreiseSuite.CONFIG.BESTPREISE_WEIGHT_RECORD === 0.0")
assert '100% Med' in page.locator('#tp-bar-weight-btn').inner_text()
Expand Down
30 changes: 23 additions & 7 deletions userscripts/toppreise/toppreise.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -1054,11 +1054,25 @@ const SHADOW_MODAL_STYLES = `
if (!str) return 0;
let clean = str.replace(/[.–\-]\s*$/g, '.00');
clean = clean.replace(/[^\d,.]/g, '').replace(/['’\s]/g, '');
if (/\d+\.\d{3},\d{2}/.test(clean)) {
clean = clean.replace(/\./g, '').replace(',', '.');

const lastComma = clean.lastIndexOf(',');
const lastDot = clean.lastIndexOf('.');
const lastSeparator = Math.max(lastComma, lastDot);

if (lastSeparator === -1) {
return parseFloat(clean) || 0;
}

const digitsAfterSeparator = clean.length - lastSeparator - 1;

if (digitsAfterSeparator === 3) {
clean = clean.replace(/[.,]/g, '');
} else {
clean = clean.replace(',', '.');
const before = clean.substring(0, lastSeparator).replace(/[.,]/g, '');
const after = clean.substring(lastSeparator + 1);
clean = before + '.' + after;
}

return parseFloat(clean) || 0;
};

Expand Down Expand Up @@ -3219,13 +3233,14 @@ const SHADOW_MODAL_STYLES = `
} else if (!activeFetches.has(pid)) {
isProcessingDetail = true;
try {
const fetchedStats = await fetchSingleProductPriceStats(pid);
if (fetchedStats) {
processProductDetailPage();
}
await fetchSingleProductPriceStats(pid);
} finally {
isProcessingDetail = false;
}
const fetchedStats = getCachedPriceStats(pid);
if (fetchedStats) {
processProductDetailPage();
}
}
}

Expand Down Expand Up @@ -3972,6 +3987,7 @@ const SHADOW_MODAL_STYLES = `
runBestpreiseScan,
cancelBestpreiseScan,
saveConfigKey,
parsePrice,
CONFIG
};
}
Expand Down
Loading