import random
from dataclasses import dataclass
from typing import List
@dataclass
class ProductPeriod:
id: str
_from: int
_to: int
@dataclass
class Item:
period: ProductPeriod
value: float
@staticmethod
def random() -> 'Item':
product_id = str(random.randint(100000, 999999))
start = random.randint(0, 10)
duration = random.randint(0, 5)
end = start + duration
value = random.uniform(0, 10)
return Item(
period=ProductPeriod(
id=product_id,
_from=start,
_to=end
),
value=value
)
@staticmethod
def random_non_overlapping(count: int) -> List['Item']:
items = []
current_day = 0
for _ in range(count):
product_id = str(random.randint(100000, 999999))
if random.random() < 0.5:
gap = 0
else:
gap = random.randint(1, 3)
start = current_day + gap
duration = random.randint(2, 5)
end = start + duration
value = random.uniform(0, 10)
items.append(Item(
period=ProductPeriod(
id=product_id,
_from=start,
_to=end
),
value=value
))
current_day = end
return items
@staticmethod
def random_overlapping(count: int) -> List['Item']:
items = []
current_day = 0
for _ in range(count):
product_id = str(random.randint(100000, 999999))
start = current_day + random.randint(-3, 2)
start = max(0, start)
duration = random.randint(2, 5)
end = start + duration
value = random.uniform(0, 10)
items.append(Item(
period=ProductPeriod(
id=product_id,
_from=start,
_to=end
),
value=value
))
current_day = start + random.randint(1, 3)
return items
@staticmethod
def same_price_back_to_back(same_price_count: int, same_price_value: float = 5.0) -> List['Item']:
items = []
current_day = 0
for _ in range(same_price_count):
product_id = str(random.randint(100000, 999999))
duration = random.randint(2, 5)
start = current_day
end = start + duration
items.append(Item(
period=ProductPeriod(
id=product_id,
_from=start,
_to=end
),
value=same_price_value
))
current_day = end
product_id = str(random.randint(100000, 999999))
duration = random.randint(2, 5)
start = current_day
end = start + duration
different_price = random.uniform(6.0, 9.0)
items.append(Item(
period=ProductPeriod(
id=product_id,
_from=start,
_to=end
),
value=different_price
))
return items
@staticmethod
def grouped_back_to_back(groups: List[tuple[int, float]]) -> List['Item']:
items = []
current_day = 0
for count, price in groups:
for _ in range(count):
product_id = str(random.randint(100000, 999999))
duration = random.randint(2, 5)
start = current_day
end = start + duration
items.append(Item(
period=ProductPeriod(
id=product_id,
_from=start,
_to=end
),
value=price
))
current_day = end
return items
@staticmethod
def grouped_with_gaps(groups: List[tuple[int, float]]) -> List['Item']:
items = []
current_day = 0
for count, price in groups:
for _ in range(count):
product_id = str(random.randint(100000, 999999))
if random.random() < 0.5:
gap = 0
else:
gap = random.randint(1, 3)
start = current_day + gap
duration = random.randint(2, 5)
end = start + duration
items.append(Item(
period=ProductPeriod(
id=product_id,
_from=start,
_to=end
),
value=price
))
current_day = end
return items
def pil(collection: List[Item]) -> tuple[Item, List[Item], ProductPeriod]:
sorted_collection = sorted(collection, key=lambda x: x.period._from, reverse=True)
current = sorted_collection[0]
latest_total_before_current_idx = next(
(i for i, c in enumerate(sorted_collection[1:], start=1) if c.value != current.value),
None
)
if latest_total_before_current_idx is None:
empty_period = ProductPeriod(id="pil", _from=0, _to=0)
return current, sorted_collection, empty_period
latest_total_before_current = sorted_collection[latest_total_before_current_idx-1]
pil_period = ProductPeriod(
id="pil",
_from=latest_total_before_current.period._from - 10,
_to=latest_total_before_current.period._from
)
overlapping_items = [
c for c in sorted_collection
if periods_overlap(c.period, pil_period)
]
if overlapping_items:
return min(overlapping_items, key=lambda c: c.value), sorted_collection, pil_period
return current, sorted_collection, pil_period
def periods_overlap(p1: ProductPeriod, p2: ProductPeriod) -> bool:
if p1._from == p1._to or p2._from == p2._to:
return False
earliest_period, latest_period = (p1, p2) if p1._from < p2._from else (p2, p1)
return earliest_period._to >= latest_period._from
def visualize_period(from_day: int, to_day: int, max_days: int = 200) -> str:
timeline = [' '] * max_days
for i in range(from_day, min(to_day, max_days)):
timeline[i] = '-'
if from_day < max_days:
timeline[from_day] = '|'
if to_day < max_days and to_day > from_day:
timeline[to_day] = '|'
return ''.join(timeline[:max_days])
def print_item(item: Item, label: str = "", highlight: bool = False, show_visual: bool = True):
marker = ">>> " if highlight else " "
duration = item.period._to - item.period._from
print(f"{marker}{label}")
print(f"{marker} Period ID: {item.period.id}")
print(f"{marker} From: {item.period._from}")
print(f"{marker} To: {item.period._to}")
print(f"{marker} Duration: {duration} days")
print(f"{marker} Value: {item.value:.2f}")
if show_visual:
visual = visualize_period(item.period._from, item.period._to)
print(f"{marker} Timeline: {visual}")
print()
def print_collection_visual(pil_result: Item, collection: List[Item], pil_period: ProductPeriod):
max_day = max(item.period._to for item in collection)
if pil_period and pil_period._from != pil_period._to:
visual = visualize_period(pil_period._from, pil_period._to, max_day + 1)
print(f"\033[91m{visual} PIL PERIOD \033[0m")
print()
for i, item in enumerate(collection):
visual = visualize_period(item.period._from, item.period._to, max_day + 1)
marker = " <-- PIL" if pil_result and item == pil_result else ""
print(f"{visual} {item.value:.2f} {marker}")
if __name__ == "__main__":
for _ in range(15):
print("-" * 50)
random_collection = Item.grouped_with_gaps([(3, 8.0), (2, 7.5), (5, 5)])
print_collection_visual(*pil(random_collection))