SPB Git forge

spb/rent-ka

Public
8commits 1branches 0releases
7.4 MBsize
maindefault branch
19 days agolast push
Python 68.8% TypeScript 18.6% CSS 8.7% JavaScript 3.3% HTML 0.6%
15.2 KB · 347 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Rent-Ka — tests de la couche de normalisation commune (rentka/normalize.py)3# -----------------------------------------------------------------------------4from datetime import date56from rentka.normalize import (7    clean_address,8    extract_details,9    normalize_unit_type,10    parse_area_sqft,11    parse_availability_date,12    parse_price,13    price_is_from,14)15from rentka.schema import Listing1617TODAY = date(2026, 8, 8)181920# -- prix ---------------------------------------------------------------------2122def test_parse_price_formats():23    assert parse_price("1 195 $/mois") == 119524    assert parse_price("À partir de 1 250,00$") == 125025    assert parse_price("$1,195") == 119526    assert parse_price("995$ - 1 200$") == 995          # le plus bas27    assert parse_price("1195,50 $") == 1195.528    assert parse_price("Contactez-nous") is None29    assert parse_price("") is None30    assert parse_price("50 $") is None                  # hors plage plausible31    assert parse_price("4 1/2 au 2e étage") is None     # pas de $323334def test_price_is_from():35    assert price_is_from("À partir de 995$")36    assert price_is_from("From $1,200")37    assert not price_is_from("1 195 $/mois")383940# -- type d'unité ---------------------------------------------------------------4142def test_normalize_unit_type():43    # Québec "n½" input converts to the English bedroom enum (n½ = n-2 bed)44    assert normalize_unit_type("4 1/2") == "2 bedrooms"45    assert normalize_unit_type("4½") == "2 bedrooms"46    assert normalize_unit_type("4,5") == "2 bedrooms"47    assert normalize_unit_type("5 1/5") == "3 bedrooms"   # frequent typo48    assert normalize_unit_type("3½") == "1 bedroom"49    assert normalize_unit_type("2 chambres") == "2 bedrooms"50    assert normalize_unit_type("3 bedrooms") == "3 bedrooms"51    assert normalize_unit_type("1 bedroom") == "1 bedroom"52    assert normalize_unit_type("6 1/2") == "4 bedrooms"53    assert normalize_unit_type("7½") == "5+ bedrooms"54    assert normalize_unit_type("Studio") == "Studio"55    assert normalize_unit_type("bachelor") == "Studio"56    assert normalize_unit_type("MDV") == "Townhouse"57    assert normalize_unit_type("maison de ville") == "Townhouse"58    assert normalize_unit_type("townhouse") == "Townhouse"59    assert normalize_unit_type("Chambre") == "Room"60    assert normalize_unit_type("room") == "Room"61    assert normalize_unit_type("condo") == "Condo"62    assert normalize_unit_type("") == ""636465# -- disponibilité ---------------------------------------------------------------6667def test_parse_availability_now():68    for raw in ("Libre immédiatement", "Maintenant", "Disponible",69                "Available now", "Occupation immédiate"):70        assert parse_availability_date(raw, TODAY) == "now", raw717273def test_parse_availability_dates():74    assert parse_availability_date("1er juillet 2027", TODAY) == "2027-07-01"75    assert parse_availability_date("Disponibilité : 01 décembre 2026", TODAY) == "2026-12-01"76    assert parse_availability_date("octobre 2026", TODAY) == "2026-10-01"77    assert parse_availability_date("2026-09-15", TODAY) == "2026-09-15"78    assert parse_availability_date("01/11/2026", TODAY) == "2026-11-01"79    assert parse_availability_date("December 1, 2026", TODAY) == "2026-12-01"808182def test_parse_availability_year_inference():83    # sans année : prochaine occurrence, ou « now » si déjà passée84    assert parse_availability_date("1er septembre", TODAY) == "2026-09-01"85    assert parse_availability_date("1er juillet", TODAY) == "now"86    # date explicite passée -> le logement est libre87    assert parse_availability_date("1er juillet 2026", TODAY) == "now"888990def test_parse_availability_unknown():91    assert parse_availability_date("", TODAY) is None92    assert parse_availability_date("Sur demande", TODAY) is None93    # frontières de mots : « rénové » ≠ novembre, « maison » ≠ mai94    assert parse_availability_date("Logement rénové", TODAY) is None95    assert parse_availability_date("Grande maison", TODAY) is None969798# -- superficie ---------------------------------------------------------------99100def test_parse_area():101    assert parse_area_sqft("850 pi²") == 850102    assert parse_area_sqft("850 pi2") == 850103    assert parse_area_sqft("1 050 sq. ft.") == 1050104    assert parse_area_sqft("78 m²") == round(78 * 10.7639)105    assert parse_area_sqft("aucune mention") is None106    assert parse_area_sqft("2 pi²") is None              # hors plage107108109# -- adresse ---------------------------------------------------------------110111def test_clean_address():112    assert clean_address("  555,   rue Exemple ,Québec ") == "555, rue Exemple, Québec"113    assert clean_address("") == ""114115116# -- détails structurés ---------------------------------------------------------117118def test_extract_details_inclusions():119    d = extract_details(["Chauffage inclus", "Eau chaude", "Lave-vaisselle"],120                        "Stationnement intérieur disponible. Animaux acceptés.")121    assert d["inclusions"]["heating"] is True122    assert d["inclusions"]["hot_water"] is True123    assert d["appliances"]["dishwasher"] is True124    assert d["parking"]["available"] is True125    assert d["parking"]["type"] == "intérieur"126    assert d["pets"] == "yes"127128129def test_extract_details_negations():130    d = extract_details([], "Pas d'animaux. Logement non-fumeur, non meublé. "131                            "Entrées laveuse-sécheuse.")132    assert d["pets"] == "no"133    assert d["smoking"] is False134    assert d["furnished"] is False135    assert d["appliances"]["washer_dryer_hookup"] is True136    assert "washer_dryer" not in d["appliances"]137138139def test_extract_details_edge_cases_connecteurs():140    # « Non chauffé et éclairé » : chauffage NON inclus, électricité incluse141    d = extract_details(["Non chauffé et éclairé"], "")142    assert d["inclusions"]["heating"] is False143    assert d["inclusions"]["electricity"] is True144    # « Chats autorisés (*sous conditions) » -> conditions145    d = extract_details(["Chats autorisés (*sous conditions)"], "")146    assert d["pets"] == "conditions"147    # électroménagers en option payante : ne rien affirmer148    d = extract_details(["Électroménagers sur demande ($)"], "")149    assert "appliances" not in d150    # anglais : refrigerator / Cooling: Central Air reconnus,151    # « Heating: Forced Air » = type de chauffage, pas une inclusion152    d = extract_details(["Refrigerator", "Cooling: Central Air", "Heating: Forced Air"], "")153    assert d["appliances"]["fridge"] is True154    assert d["ac"] is True155    assert "inclusions" not in d156157158def test_extract_details_edge_cases_lot2():159    # « Wifi (optionnel) » = option payante, pas une inclusion160    d = extract_details(["Wifi (optionnel)"], "")161    assert "inclusions" not in d162    # « Les chiens ne sont pas permis » -> sous conditions (chats tolérés)163    d = extract_details([], "Les chiens ne sont pas permis dans nos propriétés.")164    assert d["pets"] == "conditions"165    # stationnement mixte166    d = extract_details(["Stationnement extérieur et intérieur"], "")167    assert d["parking"]["type"] == "intérieur et extérieur"168    # étage ignoré dans une description multi-unités, retenu en commodité169    d = extract_details([], "Grand 4½ au 3e étage d'un immeuble de 12 logements.")170    assert "floor" not in d171    d = extract_details(["3e étage"], "")172    assert d["floor"] == 3173174175def test_extract_details_pets_both_species():176    # « Chiens et chats acceptés » = oui (pas « conditions »)177    assert extract_details(["Chiens et chats acceptés"], "")["pets"] == "yes"178    assert extract_details(["Chats et chiens admis"], "")["pets"] == "yes"179    # « Chats acceptés » seul reste « conditions »180    assert extract_details(["Chats acceptés"], "")["pets"] == "conditions"181182183def test_parse_availability_english_month_first():184    assert parse_availability_date("Available September 17", TODAY) == "2026-09-17"185    assert parse_availability_date("April 1st, 2027", TODAY) == "2027-04-01"186    assert parse_availability_date("juillet 2027", TODAY) == "2027-07-01"  # pas « jour 20 »187188189def test_parse_area_context_and_thousands():190    # la superficie du balcon n'est pas celle du logement191    assert parse_area_sqft("1134 pi², balcon 277 pi²") == 1134192    assert parse_area_sqft("1,607 sq. ft.") == 1607193194195def test_extract_details_edge_cases_lots_9_a_13():196    # « washer » ne doit pas matcher « dishwasher »197    d = extract_details(["Dishwasher"], "")198    assert d["appliances"] == {"dishwasher": True}199    # entrée lave-vaisselle = branchement, pas l'appareil200    d = extract_details(["Entrée lave-vaisselle"], "")201    assert d["appliances"].get("dishwasher") is not True202    assert d["appliances"]["dishwasher_hookup"] is True203    # coquille fréquente « ascenceur »204    assert extract_details(["Ascenceur"], "")["elevator"] is True205    # pets : formulations variées206    assert extract_details(["Chien interdit"], "")["pets"] == "conditions"207    assert extract_details(["Chiens permis"], "")["pets"] == "yes"208    assert extract_details(["Animaux de compagnie acceptés"], "")["pets"] == "yes"209    assert extract_details([], "Animaux permis : Non")["pets"] == "no"210    assert extract_details([], "Animaux acceptés (20lb et moins)")["pets"] == "conditions"211    # anglais : heat/hydro/terrace/laundry facilities ; heat pump ≠ inclusion212    d = extract_details(["Heat", "Hydro", "Rooftop terrace", "Laundry facilities"], "")213    assert d["inclusions"]["heating"] is True214    assert d["inclusions"]["electricity"] is True215    assert d["balcony"] is True and d["laundry"] is True216    # semi-meublé : nuance conservée, pas un booléen217    assert extract_details(["Semi-meublé"], "")["furnished"] == "semi"218    # vélo ≠ stationnement auto ; « ($) » = payant219    assert "parking" not in extract_details(["Stationnement à vélo intérieur"], "")220    assert "inclusions" not in extract_details(["Wi-Fi ($)"], "")221222223def test_extract_details_edge_cases_lots_4_et_7():224    # « chauffé » qualifiant autre chose que le logement225    d = extract_details(["Stationnement intérieur non chauffé"], "")226    assert "inclusions" not in d227    assert d["parking"]["available"] is True228    d = extract_details(["Piscine extérieure chauffée"], "")229    assert "inclusions" not in d and d["pool"] is True230    # aires communes ≠ unité231    d = extract_details(["Terrasse commune avec BBQ", "WI-FI dans les aires communes"], "")232    assert "balcony" not in d and "inclusions" not in d233    # étage d'une installation ≠ étage de l'unité234    assert "floor" not in extract_details(["Salon lounge au 25e étage"], "")235    # négations et « rien d'inclus »236    assert extract_details(["Pas de balcon"], "")["balcony"] is False237    d = extract_details([], "Rien d'inclus.")238    assert d["inclusions"]["heating"] is False239    assert extract_details([], "Les animaux sont interdits.")["pets"] == "no"240    # option payante « moyennant des frais mensuels »241    d = extract_details(["Électroménagers (moyennant des frais mensuels)"], "")242    assert "appliances" not in d243244245def test_normalize_unit_type_word_numbers():246    assert normalize_unit_type("Une chambre") == "1 bedroom"247    assert normalize_unit_type("Two bedrooms") == "2 bedrooms"248249250def test_extract_details_edge_cases_lot6():251    # animal au singulier252    assert extract_details([], "Votre animal est le bienvenu !")["pets"] == "yes"253    # nom de promotion ≠ inclusion254    d = extract_details([], "Promotion : Crédit de 1 000 $ Éclaire à la signature.")255    assert "inclusions" not in d256    # prix de stationnement complet, formats FR et EN257    d = extract_details([], "Stationnement disponible : 2 espaces pour seulement 150 $.")258    assert d["parking"]["price"] == 150259    d = extract_details([], "Parking available for $85 per month.")260    assert d["parking"]["price"] == 85261262263def test_extract_details_edge_cases_lot5():264    # « à la charge des locataires » = NON inclus265    d = extract_details([], "Chauffage et électricité à la charge des locataires.")266    assert d["inclusions"]["heating"] is False267    assert d["inclusions"]["electricity"] is False268    # « Actuellement vacant » = libre maintenant269    assert parse_availability_date("Actuellement vacant", TODAY) == "now"270271272def test_extract_details_edge_cases_lot8():273    # « non-chauffé, non-éclairé » avec traits d'union274    d = extract_details(["Non-chauffé, non-éclairé"], "")275    assert d["inclusions"]["heating"] is False276    assert d["inclusions"]["electricity"] is False277    # branchement combiné : ni laveuse ni lave-vaisselle fournis278    d = extract_details(["Entrées lave-vaisselle et laveuse/sécheuse"], "")279    assert d["appliances"].get("washer_dryer") is not True280    assert d["appliances"].get("dishwasher") is not True281    assert d["appliances"]["washer_dryer_hookup"] is True282    # « refusés sauf un chat » = conditions283    assert extract_details([], "Animaux refusés, à l'exception d'un chat.")["pets"] == "conditions"284    # restriction entre parenthèses285    assert "ac" not in extract_details(["Appareil de climatisation (Sans utilisation)"], "")286287288def test_price_from_range():289    assert price_is_from("de 1 650 $ à 2 293 $")290291292def test_extract_details_unknown_stays_absent():293    d = extract_details(["Balcon"], "")294    assert d.get("balcony") is True295    assert "pets" not in d296    assert "smoking" not in d297    assert "inclusions" not in d298299300def test_extract_details_contact():301    d = extract_details([], "Pour info : 418 555-1234 ou louer@exemple.ca")302    assert d["contact"]["phone"] == "418-555-1234"303    assert d["contact"]["email"] == "louer@exemple.ca"304305306# -- finalize (enrichissement de bout en bout) -----------------------------------307308def test_listing_finalize():309    l = Listing(310        source="test", external_id="1", url="https://x/1",311        title="Le Test — 4 1/2",312        address="555,rue Exemple,  Québec",313        unit_type="2 chambres",314        price_label="À partir de 1 195 $/mois",315        availability="1er décembre 2026",316        description="Superbe 4½ de 850 pi². Chauffage et eau chaude inclus. "317                    "Animaux refusés. Info : (418) 555-9999.",318        amenities=["Balcon", "Ascenseur", "Piscine intérieure"],319    ).finalize()320    assert l.unit_type == "2 bedrooms"321    assert l.price == 1195322    assert l.availability_date == "2026-12-01"323    assert l.area_sqft == 850324    assert l.pets == "no"325    assert l.address == "555, rue Exemple, Québec"326    assert l.details["price_from"] is True327    assert l.details["balcony"] is True328    assert l.details["elevator"] is True329    assert l.details["pool"] is True330    assert l.details["inclusions"]["heating"] is True331    assert l.details["contact"]["phone"] == "418-555-9999"332333334def test_listing_finalize_respects_connector_values():335    l = Listing(source="t", external_id="2", url="u",336                price=999.0, price_label="1 500 $",337                pets="oui", furnished=True,338                availability_date="2027-01-01", availability="maintenant",339                details={"parking": {"available": True, "price": 60.0}})340    l.finalize()341    assert l.price == 999.0                       # valeur connecteur conservée342    assert l.availability_date == "2027-01-01"343    # finalize() normalizes the legacy French value to the English canon344    assert l.pets == "yes"345    assert l.furnished is True346    assert l.details["parking"]["price"] == 60.0347