fix(mera-public): защиты вместо тавтологий в coverage-тексте и выборе раундов
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m9s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m9s
Проверка «уверенные + средние + пустые = 100» была верна по определению средней доли и упасть не могла. Инвариант перенесён на сам замер: уверенные и пустые обязаны оставлять место третьей доле. Если её всё-таки не останется (перезамер 85 + 15), текст теперь двухчастный — «Ещё в 0 данных меньше» отрендериться не может. `street_name !== null` считал пустую строку названием улицы, и вторая такая строка вылетала из игры как «повтор». Проверка на пустоту.
This commit is contained in:
parent
c093212eae
commit
ef166eaf21
4 changed files with 55 additions and 10 deletions
|
|
@ -170,7 +170,12 @@ describe("describeCityExpectation", () => {
|
|||
expect(nums, `${measure.city}: нет доли уверенных`).toContain(measure.confidentPct);
|
||||
expect(nums, `${measure.city}: нет доли «посчитаем, но шире»`).toContain(thin);
|
||||
expect(nums, `${measure.city}: нет доли пустых когорт`).toContain(measure.emptyPct);
|
||||
expect(measure.confidentPct + thin + measure.emptyPct).toBe(100);
|
||||
// Инвариант ЗАМЕРА, а не арифметики: уверенные и пустые обязаны
|
||||
// оставлять место третьей доле, иначе рендер печатает «Ещё в 0».
|
||||
expect(
|
||||
measure.confidentPct + measure.emptyPct,
|
||||
`${measure.city}: средней доле не осталось места`,
|
||||
).toBeLessThan(100);
|
||||
// Про пустые когорты сказано, что мы НЕ считаем, — иначе текст обещает
|
||||
// расчёт там, где следующий же экран в нём откажет.
|
||||
expect(said?.text, `${measure.city}: обещан расчёт на пустой когорте`).toMatch(
|
||||
|
|
@ -179,6 +184,20 @@ describe("describeCityExpectation", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("когда средней доли не осталось — предложения про неё нет вовсе", () => {
|
||||
// Перезамер вида 85 уверенных + 15 пустых законен, и «Ещё в 0 данных
|
||||
// меньше» на нём было бы сообщением о доле, которой не существует.
|
||||
const said = describeCityExpectation("Ревда", {
|
||||
city: "Ревда",
|
||||
confidentPct: 85,
|
||||
emptyPct: 15,
|
||||
sampleN: 100,
|
||||
});
|
||||
expect(said?.text).not.toMatch(/Ещё в 0|в 0 /);
|
||||
expect((said?.text.match(/\d+/g) ?? []).map(Number)).toEqual([85, 100, 15]);
|
||||
expect(said?.text).toMatch(/считать не станем/);
|
||||
});
|
||||
|
||||
it("неизвестный город не выдумывает величину", () => {
|
||||
expect(describeCityExpectation("Москва")).toBeNull();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,6 +67,14 @@ describe("pickVariedDeals", () => {
|
|||
expect(pickVariedDeals(noStreet, 2).map((d) => d.area_m2)).toEqual([38, 58]);
|
||||
});
|
||||
|
||||
it("пустая строка в street_name — не ключ улицы, а её отсутствие", () => {
|
||||
// `!== null` считал "" настоящим названием, и вторая такая строка
|
||||
// вылетала как «повтор улицы» — повтора здесь нет, есть два молчания.
|
||||
// Третья строка — та, которую при этом брали ВМЕСТО второй.
|
||||
const blank = [deal(1, "", 38), deal(2, "", 58), deal(2, "улица Мира", 60)];
|
||||
expect(pickVariedDeals(blank, 2).map((d) => d.area_m2)).toEqual([38, 58]);
|
||||
});
|
||||
|
||||
it("выбор детерминирован — страница рендерится на сервере", () => {
|
||||
// Разный набор на сервере и в браузере — это разъехавшаяся гидрация,
|
||||
// поэтому никакого случайного выбора здесь быть не может.
|
||||
|
|
|
|||
|
|
@ -151,11 +151,13 @@ export function pickVariedDeals(deals: readonly ShowcaseDeal[], n: number): Show
|
|||
if (picked.length >= n) break;
|
||||
if (rooms.has(deal.rooms)) continue;
|
||||
// Улицы нет примерно у 8% строк — такая строка не «повторяет улицу», она
|
||||
// про неё молчит, и держать её из-за этого вне игры не за что.
|
||||
if (deal.street_name !== null && streets.has(deal.street_name)) continue;
|
||||
// про неё молчит, и держать её из-за этого вне игры не за что. Проверка
|
||||
// именно на пустоту, а не на `!== null`: пустая строка — то же самое
|
||||
// молчание, и второй такой строке нельзя вылетать как «повтору улицы».
|
||||
if (deal.street_name && streets.has(deal.street_name)) continue;
|
||||
picked.push(deal);
|
||||
rooms.add(deal.rooms);
|
||||
if (deal.street_name !== null) streets.add(deal.street_name);
|
||||
if (deal.street_name) streets.add(deal.street_name);
|
||||
}
|
||||
for (const deal of deals) {
|
||||
if (picked.length >= n) break;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
*/
|
||||
|
||||
import { CITY_COVERAGE, CITY_COVERAGE_SOURCE } from "./landing-facts";
|
||||
import type { CityCoverageMeasure } from "./landing-facts";
|
||||
import type { CoverageProbe } from "./public-api";
|
||||
|
||||
export interface CoverageTile {
|
||||
|
|
@ -104,8 +105,13 @@ export interface CityExpectation {
|
|||
* что выбрасывать города из списка не за что. Разница между ними
|
||||
* количественная — её и показываем числом, а не отсутствием опции.
|
||||
*/
|
||||
export function describeCityExpectation(city: string): CityExpectation | null {
|
||||
const measure = CITY_COVERAGE.find((m) => m.city === city);
|
||||
export function describeCityExpectation(
|
||||
city: string,
|
||||
// Замер вторым аргументом — чтобы ветку «средней доли не осталось» можно
|
||||
// было проверить, не подделывая CITY_COVERAGE: сегодня такого города в
|
||||
// замере нет, а появиться он может при первом же перезамере.
|
||||
measure: CityCoverageMeasure | undefined = CITY_COVERAGE.find((m) => m.city === city),
|
||||
): CityExpectation | null {
|
||||
if (!measure) return null;
|
||||
|
||||
// Остаток от уверенных — НЕ однородный: в него входят и пустые пробы, где
|
||||
|
|
@ -113,12 +119,22 @@ export function describeCityExpectation(city: string): CityExpectation | null {
|
|||
// них тоже, а `describeCoverage` в той же ситуации отвечает «не считаем» —
|
||||
// две наши же строки спорили друг с другом. Делим остаток надвое.
|
||||
const thin = 100 - measure.confidentPct - measure.emptyPct;
|
||||
const head =
|
||||
`В городе ${city} выборки хватает на уверенный расчёт в ${measure.confidentPct} ` +
|
||||
"проверках из 100. ";
|
||||
// Средней доли может не остаться вовсе (перезамер вида 85 уверенных + 15
|
||||
// пустых) — тогда предложение про неё выпадает целиком. «Ещё в 0 данных
|
||||
// меньше» было бы не округлением, а сообщением о доле, которой нет.
|
||||
const empty = thin > 0 ? measure.emptyPct : 100 - measure.confidentPct;
|
||||
const tail =
|
||||
`В оставшихся ${empty} рядом не находится ни одной похожей квартиры — ` +
|
||||
"так и скажем, а считать не станем.";
|
||||
return {
|
||||
text:
|
||||
`В городе ${city} выборки хватает на уверенный расчёт в ${measure.confidentPct} проверках ` +
|
||||
`из 100. Ещё в ${thin} данных меньше — расчёт мы сделаем, но разброс будет шире, и в ` +
|
||||
`ответе это будет написано. В оставшихся ${measure.emptyPct} рядом не находится ни одной ` +
|
||||
"похожей квартиры — так и скажем, а считать не станем.",
|
||||
thin > 0
|
||||
? `${head}Ещё в ${thin} данных меньше — расчёт мы сделаем, но разброс будет шире, и в ` +
|
||||
`ответе это будет написано. ${tail}`
|
||||
: head + tail,
|
||||
source: `${CITY_COVERAGE_SOURCE}. Прогнано адресов: ${measure.sampleN}.`,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue