regle.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import datetime
  2. from functools import partial
  3. from math import ceil
  4. from django.contrib.auth.models import User
  5. from django.db import models
  6. from djangotools.common.types import parse_date, date_to_string
  7. from djangotools.common.utils import get_today
  8. from baby.app.models.const import get_average_month, get_min_period, get_max_period
  9. class Cycles(list):
  10. def __init__(self, user, start, end):
  11. super().__init__()
  12. self.user = user
  13. self.start = start
  14. self.end = end
  15. def iter_dates(self, start=None, end=None):
  16. if not self: return
  17. total = []
  18. start = start or self.start or self[0].start
  19. end = end or self.end or self[-1].end
  20. for cycle in self:
  21. if cycle.end < start: continue
  22. for date, cur in cycle:
  23. if date<start: continue
  24. if date>end: return
  25. total.append(date)
  26. yield date, cur
  27. return
  28. def get_periods(self, with_doubtfull):
  29. return [
  30. x.length for x in self
  31. if with_doubtfull or not x.doubtfull
  32. ]
  33. def resolve_doubtfull(self):
  34. cycles = Cycles(self.user, start=self.start, end=self.end)
  35. no_doubtfull_periods = self.get_periods(with_doubtfull=False)
  36. average = (sum(no_doubtfull_periods) / len(no_doubtfull_periods)) if no_doubtfull_periods else None
  37. for cycle in self:
  38. cycles.extend(cycle.resolve_doubtfull(average))
  39. return cycles
  40. def copy(self):
  41. ret = Cycles(user=self.user, start=self.start, end=self.end)
  42. ret.extend(self)
  43. return ret
  44. def get_average_period(user, start, end):
  45. cycle = Cycle.from_date_range(user, start, end)
  46. cycle = cycle.resolve_doubtfull()
  47. periods = cycle.get_periods(False)
  48. if not periods: return None
  49. return sum(periods) / len(periods)
  50. def get_average_period_from_today(user, monthes, ref):
  51. ref = parse_date(ref)
  52. start = ref - datetime.timedelta(days=monthes*30)
  53. return get_average_period(user, start, ref)
  54. class Cycle:
  55. DEBUT="debut"
  56. OVULATION="ovulation"
  57. FERTILITE="fertilite"
  58. DEBUT_SIMULATED="debut_simu"
  59. TODAY="today"
  60. def __init__(self, user):
  61. self.user = user
  62. self.start : datetime.date = None
  63. self.end : datetime.date = None
  64. self.length : int = None
  65. self.min_cycle : int = None
  66. self.max_cycle : int = None
  67. self.fertility_start_offset : int = None
  68. self.fertility_start : datetime.date = None
  69. self.fertility_end_offset : int = None
  70. self.fertility_end : datetime.date = None
  71. self.ovulation_offset : int = None
  72. self.ovulation : datetime.date = None
  73. self.doubtfull : bool = None
  74. self.simulated : bool = None
  75. def as_list(self, with_date=False):
  76. today = get_today()
  77. def add_tag(ret, offset_day, tag):
  78. if with_date:
  79. ret[offset_day][1].append(tag)
  80. else:
  81. ret[offset_day].append(tag)
  82. if with_date:
  83. ret = [(self.start + datetime.timedelta(days=i), []) for i in range(self.length)]
  84. else:
  85. ret = [[] for _ in range(self.length)]
  86. add_tag(ret, 0, self.DEBUT_SIMULATED if self.simulated else self.DEBUT)
  87. if self.length >= 14 and not self.doubtfull:
  88. add_tag(ret, self.length - 14, self.OVULATION)
  89. if self.fertility_start_offset and not self.doubtfull:
  90. [add_tag(ret, i-1, self.FERTILITE) for i in
  91. range(self.fertility_start_offset, self.fertility_end_offset + 1) if 0 < i < self.length]
  92. if self.start <= today < self.end:
  93. offset = (today - self.start).days
  94. add_tag(ret, offset, self.TODAY)
  95. return ret
  96. def resolve_doubtfull(self, average):
  97. cycles = Cycles(self.user, self.start, self.end)
  98. if not self.doubtfull:
  99. cycles.append(self)
  100. return cycles
  101. if average is None or average >= self.length:
  102. return cycles
  103. n_periods = round(self.length / average)
  104. period = self.length / n_periods
  105. start = self.start
  106. for i in range(1, 1 + n_periods - 1):
  107. end = self.start + datetime.timedelta(days=round(i * period))
  108. cycles.append(Cycle.create(self.user, start, end))
  109. start = end
  110. cycles.append(Cycle.create(self.user, start, self.end))
  111. return cycles
  112. def __iter__(self):
  113. return iter(self.as_list(with_date=True))
  114. def __repr__(self):
  115. doubtfull = " (!) " if self.doubtfull else ""
  116. return f"<Cycle {date_to_string(self.start)} to {date_to_string(self.end)} ({self.length}){doubtfull}>"
  117. def __str__(self):
  118. return self.__repr__()
  119. @property
  120. def json(self):
  121. return {
  122. "start" : date_to_string(self.start),
  123. "end" : date_to_string(self.end),
  124. "length" : self.length,
  125. "doubtfull" : self.doubtfull,
  126. "simulated" : self.simulated,
  127. "ovulation" : date_to_string(self.ovulation),
  128. "fertility" : [date_to_string(self.fertility_start),date_to_string(self.fertility_end)],
  129. "ovulation_offset" : self.ovulation_offset,
  130. "fertility_offset" : [self.fertility_start_offset, self.fertility_end_offset],
  131. }
  132. def _init(self):
  133. assert self.start
  134. if not self.end:
  135. assert self.length
  136. self.end = self.start + datetime.timedelta(days=self.length)
  137. elif not self.length:
  138. assert self.end
  139. self.length = (self.end - self.start).days
  140. else:
  141. return
  142. self._fertility_init()
  143. if self.fertility_start_offset is not None:
  144. self.fertility_start = self.start + datetime.timedelta(days=self.fertility_start_offset-1)
  145. if self.fertility_end_offset is not None:
  146. self.fertility_end = self.start + datetime.timedelta(days=self.fertility_end_offset-1)
  147. if self.ovulation_offset is not None:
  148. self.ovulation = self.start + datetime.timedelta(days=self.ovulation_offset-1)
  149. self.doubtfull = not (get_min_period(self.user) <= self.length <= get_max_period(self.user))
  150. def _fertility_init(self):
  151. self.fertility_start_offset = self.min_cycle and (self.min_cycle - 18)
  152. self.fertility_end_offset = self.max_cycle and (self.max_cycle - 11)
  153. self.ovulation_offset = ceil(self.length - 11)
  154. @classmethod
  155. def create(cls, user, start, end=None, length=None, monthes=None, simulated=False):
  156. filter = partial(Regle.objects.filter, user=user)
  157. kwargs = {
  158. "date__lte": start
  159. }
  160. if monthes != 0:
  161. monthes = monthes if monthes is not None else get_average_month(user)
  162. kwargs["date__gte"] = start - datetime.timedelta(days=monthes*30)
  163. regles = list(filter(**kwargs ).order_by("date"))
  164. periods = Regle.objects.rgeles_to_period(regles, min_period=get_min_period(user), max_period=get_max_period(user))
  165. mini = None
  166. maxi = None
  167. if periods:
  168. mini = min(periods)
  169. maxi = max(periods)
  170. cycle = cls(user)
  171. cycle.start = start
  172. if end:
  173. cycle.end = end
  174. elif length:
  175. cycle.length = length
  176. else:
  177. raise ValueError
  178. cycle.min_cycle = mini
  179. cycle.max_cycle = maxi
  180. cycle.simulated = simulated
  181. cycle._init()
  182. return cycle
  183. @classmethod
  184. def from_day(cls, user, ref_date=None, monthes=None, today=None):
  185. today = today or datetime.date.today()
  186. date = parse_date(ref_date) or today
  187. regle = Regle.objects.last_regle_before(user, date)
  188. end = Regle.objects.next_regle_after(user, date)
  189. monthes = monthes if monthes is not None else get_average_month(user)
  190. kwargs = {"user": user}
  191. if ref_date and ref_date > today:
  192. length = get_average_period_from_today(user, monthes, today)
  193. if length is None:
  194. return None
  195. ilength = round(length)
  196. offset = int((date - regle.date).days / ilength) * ilength
  197. kwargs["start"] = regle.date + datetime.timedelta(days=int(offset))
  198. kwargs["end"] = kwargs["start"] + datetime.timedelta(days=ilength)
  199. kwargs["simulated"] = offset >= length
  200. elif regle:
  201. kwargs["start"] = regle.date
  202. if end:
  203. kwargs["end"] = end.date
  204. else:
  205. length = get_average_period_from_today(user, monthes, today)
  206. if length is not None:
  207. kwargs["length"] = round(length)
  208. else:
  209. return None
  210. elif end:
  211. length = get_average_period(user, end.date, end.date + datetime.timedelta(days=monthes*12))
  212. ilength = round(length)
  213. offset = ceil((end.date - date).days / ilength) * ilength
  214. kwargs["start"] = end.date - datetime.timedelta(days=int(offset))
  215. kwargs["end"] = kwargs["start"] + datetime.timedelta(days=ilength)
  216. kwargs["simulated"] = True
  217. if "start" not in kwargs:
  218. return None
  219. return cls.create(**kwargs)
  220. @classmethod
  221. def _from_date_range(cls, user, start, end):
  222. if start > end: start, end = end, start
  223. cycles = Cycles(user, start=start, end=end)
  224. curr = start
  225. while curr <= end+datetime.timedelta(days=1):
  226. cycle = cls.from_day(user, curr)
  227. if cycle is None:
  228. break
  229. cycles.append(cycle)
  230. curr = cycle.end + datetime.timedelta(days=1)
  231. return cycles
  232. @classmethod
  233. def from_date_range(cls, user, start, end):
  234. if start > end: start, end = end, start
  235. cycles = Cycles(user, start=start, end=end)
  236. regles = list(Regle.objects.filter(user=user, date__gte=start, date__lt=end).order_by("date"))
  237. if regles:
  238. start = regles.pop(0)
  239. while regles:
  240. end = regles.pop(0)
  241. cycle = Cycle.create(user, start.date, end.date)
  242. cycles.append(cycle)
  243. start = end
  244. return cycles
  245. @classmethod
  246. def from_month(cls, user, mont_or_date, year):
  247. if isinstance(mont_or_date, str):
  248. mont_or_date = parse_date(mont_or_date)
  249. if isinstance(mont_or_date, (datetime.date, datetime.datetime)):
  250. mont_or_date, year = mont_or_date.month, mont_or_date.year
  251. assert isinstance(mont_or_date, int) and isinstance(year, int)
  252. if year < 100: year += 2000
  253. assert 0 < mont_or_date <= 12
  254. start = datetime.date(year, mont_or_date, 1)
  255. end = start + datetime.timedelta(days=32)
  256. end = end - datetime.timedelta(days=end.day)
  257. return cls.from_date_range(user, start, end)
  258. class RegleManager(models.Manager):
  259. def last_year(self, user):
  260. return self.filter(user=user, date__gte=datetime.date.today() - datetime.timedelta(days=-395))
  261. def create(self, user, date=None):
  262. date = parse_date(date)
  263. if date is None:
  264. date = datetime.date.today()
  265. return super().create(user=user, date=date)
  266. def last_regle_before(self, user, date=None):
  267. if date is None: date = datetime.date.today()
  268. regle = self.filter(user=user, date__lte=date).order_by("-date")[:1]
  269. regle = list(regle)
  270. return regle[0] if regle else None
  271. def average_period(self, user, monthes=None, date_min=None, date_max=None):
  272. kwargs = {"user": user}
  273. if date_min or date_max:
  274. if date_min: kwargs["date__gte"] = date_min
  275. if date_max: kwargs["date__lte"] = date_max
  276. elif monthes:
  277. kwargs["date__gte"] = datetime.date.today() - datetime.timedelta(days=monthes*30)
  278. regles = self.filter(**kwargs).order_by("date")
  279. data = self.rgeles_to_period(regles)
  280. if data:
  281. return sum(data) / len(data)
  282. return None
  283. @classmethod
  284. def rgeles_to_period(cls, data, start_date=False, min_period=None, max_period=None):
  285. data = iter(data)
  286. periods = []
  287. try:
  288. last = next(data)
  289. while True:
  290. curr = next(data)
  291. dt = curr.date - last.date
  292. if start_date:
  293. dt = (last.date, dt)
  294. if (min_period is not None and dt.days<min_period or
  295. max_period is not None and dt.days>max_period):
  296. last=curr
  297. continue
  298. periods.append(dt.days)
  299. last = curr
  300. except StopIteration:
  301. pass
  302. return periods
  303. def next_regle_after(self, user, date):
  304. regle = self.filter(user=user, date__gt=date).order_by("date")[:1]
  305. regle = list(regle)
  306. return regle[0] if regle else None
  307. class Regle(models.Model):
  308. objects = RegleManager()
  309. user = models.ForeignKey(User, on_delete=models.CASCADE)
  310. date = models.DateField()
  311. DoesNotExist : Exception
  312. class Meta:
  313. unique_together = ["user", "date"]
  314. def __repr__(self):
  315. return f"<Regle {self.user.username} {self.date}>"
  316. def __str__(self):
  317. return self.__repr__()