OpeningHoursFormatter.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. <?php
  2. /**
  3. * Refer to LICENSE.txt distributed with the Temando Shipping module for notice of license
  4. */
  5. namespace Temando\Shipping\Model\Delivery;
  6. use Magento\Framework\App\ScopeResolverInterface;
  7. use Magento\Framework\Locale\ResolverInterface;
  8. use Magento\Framework\Serialize\SerializerInterface;
  9. use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
  10. use Temando\Shipping\Model\Config\ConfigAccessor;
  11. /**
  12. * Temando Delivery Location Opening Hours Formatter
  13. *
  14. * @package Temando\Shipping\Model
  15. * @author Christoph Aßmann <christoph.assmann@netresearch.de>
  16. * @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
  17. * @link https://www.temando.com/
  18. */
  19. class OpeningHoursFormatter
  20. {
  21. /**
  22. * @var ScopeResolverInterface
  23. */
  24. private $scopeResolver;
  25. /**
  26. * @var ResolverInterface
  27. */
  28. private $localeResolver;
  29. /**
  30. * @var ConfigAccessor
  31. */
  32. private $config;
  33. /**
  34. * @var TimezoneInterface
  35. */
  36. private $date;
  37. /**
  38. * @var SerializerInterface
  39. */
  40. private $serializer;
  41. /**
  42. * OpeningHoursFormatter constructor.
  43. *
  44. * @param ScopeResolverInterface $scopeResolver
  45. * @param ResolverInterface $localeResolver
  46. * @param ConfigAccessor $config
  47. * @param TimezoneInterface $date
  48. * @param SerializerInterface $serializer
  49. */
  50. public function __construct(
  51. ScopeResolverInterface $scopeResolver,
  52. ResolverInterface $localeResolver,
  53. ConfigAccessor $config,
  54. TimezoneInterface $date,
  55. SerializerInterface $serializer
  56. ) {
  57. $this->scopeResolver = $scopeResolver;
  58. $this->localeResolver = $localeResolver;
  59. $this->config = $config;
  60. $this->date = $date;
  61. $this->serializer = $serializer;
  62. }
  63. /**
  64. * Return first day of the week
  65. *
  66. * @return int
  67. */
  68. private function getFirstDay()
  69. {
  70. return (int)$this->config->getConfigValue(
  71. 'general/locale/firstday',
  72. $this->scopeResolver->getScope()->getId()
  73. );
  74. }
  75. /**
  76. * Format general opening hours, e.g.:
  77. * - Monday, Tuesday | 9:00 AM - 8:00 PM
  78. * - Montag, Dienstag | 09:00 - 20:00
  79. *
  80. * @param string[] $openingHours
  81. * @param string $locale
  82. * @return string[]
  83. */
  84. private function formatGeneralOpenings(array $openingHours, string $locale): array
  85. {
  86. $firstDay = $this->getFirstDay();
  87. // sort opening hours by day
  88. $fnDaySort = function ($dayA, $dayB) use ($firstDay, $locale) {
  89. $dayA = (int)$this->date->date($dayA, $locale, false, false)->format('w');
  90. $dayB = (int)$this->date->date($dayB, $locale, false, false)->format('w');
  91. if ($dayA < $firstDay) {
  92. $dayA = $firstDay + 7;
  93. }
  94. if ($dayB < $firstDay) {
  95. $dayB = $firstDay + 7;
  96. }
  97. return $dayA > $dayB;
  98. };
  99. // sort one day's opening hours by start time
  100. $fnTimeSort = function ($rangeA, $rangeB) {
  101. return $rangeA['from'] > $rangeB['from'];
  102. };
  103. uksort($openingHours, $fnDaySort);
  104. // aggregate days with the same opening hours
  105. $hoursMap = [];
  106. foreach ($openingHours as $day => $ranges) {
  107. usort($ranges, $fnTimeSort);
  108. $key = crc32($this->serializer->serialize($ranges));
  109. if (!isset($hoursMap[$key])) {
  110. $hoursMap[$key] = [
  111. 'days' => [],
  112. 'ranges' => $ranges,
  113. ];
  114. }
  115. $day = $this->date->date($day, $locale, false, false)->format('l');
  116. $hoursMap[$key]['days'][] = $day;
  117. }
  118. // localize times
  119. $generalOpenings = [];
  120. foreach ($hoursMap as $key => $details) {
  121. $days = implode(', ', $details['days']);
  122. $times = [];
  123. foreach ($details['ranges'] as $range) {
  124. $dateOpens = $this->date->date($range['from'], $locale, false, true);
  125. $dateCloses = $this->date->date($range['to'], $locale, false, true);
  126. $timeOpens = $this->date->formatDateTime(
  127. $dateOpens,
  128. \IntlDateFormatter::NONE,
  129. \IntlDateFormatter::SHORT,
  130. $locale,
  131. 'UTC'
  132. );
  133. $timeCloses = $this->date->formatDateTime(
  134. $dateCloses,
  135. \IntlDateFormatter::NONE,
  136. \IntlDateFormatter::SHORT,
  137. $locale,
  138. 'UTC'
  139. );
  140. $times[]= sprintf('%s - %s', $timeOpens, $timeCloses);
  141. }
  142. $generalOpenings[] = [
  143. 'days' => $days,
  144. 'times' => implode(', ', $times),
  145. ];
  146. }
  147. return $generalOpenings;
  148. }
  149. /**
  150. * Format specifics, e.g.:
  151. * - Dec 24 9:00 AM - Jan 1 8:00 PM
  152. * - Jul 4 9:00 AM - 8:00 PM
  153. * - 24.12. 09:00 - 01.01. 20:00
  154. * - 03.10. 09:00 - 20:00
  155. *
  156. * @param string[] $openingHours
  157. * @param string $locale
  158. * @param int $offset
  159. * @return string[]
  160. */
  161. private function formatSpecifics(array $openingHours, string $locale, $offset = 7): array
  162. {
  163. $formattedSpecifics = [];
  164. $today = $this->date->date();
  165. $fnDateSort = function ($dateA, $dateB) {
  166. $dateA = preg_filter('/[^\d]/', '', $dateA['from']);
  167. $dateB = preg_filter('/[^\d]/', '', $dateB['from']);
  168. return $dateA > $dateB;
  169. };
  170. usort($openingHours, $fnDateSort);
  171. foreach ($openingHours as $opening) {
  172. $dateFrom = $this->date->date($opening['from'], $locale, false, true);
  173. $dateTo = $this->date->date($opening['to'], $locale, false, true);
  174. $diff = $today->diff($dateFrom);
  175. if (!$diff->invert && ($diff->days > $offset)) {
  176. // do not display any specifics beginning more than $offset days in the future
  177. continue;
  178. }
  179. $diff = $today->diff($dateTo);
  180. if ($diff->invert) {
  181. // do not display any specifics ending in the past
  182. continue;
  183. }
  184. // extract year, will be stripped off later
  185. $dateFromYear = $dateFrom->format('Y');
  186. $dateToYear = $dateTo->format('Y');
  187. // start date, date part
  188. $dateFromDate = $this->date->formatDateTime(
  189. $dateFrom,
  190. \IntlDateFormatter::MEDIUM,
  191. \IntlDateFormatter::NONE,
  192. $locale,
  193. 'UTC'
  194. );
  195. // end date, date part
  196. $dateToDate = $this->date->formatDateTime(
  197. $dateTo,
  198. \IntlDateFormatter::MEDIUM,
  199. \IntlDateFormatter::NONE,
  200. $locale,
  201. 'UTC'
  202. );
  203. // start date, time part
  204. $dateFromTime = $timeOpens = $this->date->formatDateTime(
  205. $dateFrom,
  206. \IntlDateFormatter::NONE,
  207. \IntlDateFormatter::SHORT,
  208. $locale,
  209. 'UTC'
  210. );
  211. // end date, time part
  212. $dateToTime = $timeOpens = $this->date->formatDateTime(
  213. $dateTo,
  214. \IntlDateFormatter::NONE,
  215. \IntlDateFormatter::SHORT,
  216. $locale,
  217. 'UTC'
  218. );
  219. $dateFromDate = trim(str_replace($dateFromYear, '', $dateFromDate), ' ,');
  220. $dateToDate = trim(str_replace($dateToYear, '', $dateToDate), ' ,');
  221. $formattedDateFrom = sprintf('%s %s', $dateFromDate, $dateFromTime);
  222. $formattedDateTo = sprintf('%s %s', $dateToDate, $dateToTime);
  223. if ($dateFromDate == $dateToDate) {
  224. $formattedDateTo = $dateToTime;
  225. }
  226. $formattedSpecifics[] = [
  227. 'description' => __($opening['description']),
  228. 'from' => $formattedDateFrom,
  229. 'to' => $formattedDateTo,
  230. ];
  231. }
  232. return $formattedSpecifics;
  233. }
  234. /**
  235. * Combine and format opening hours.
  236. *
  237. * @param string[] $openingHours
  238. *
  239. * @return string[]
  240. */
  241. public function format(array $openingHours): array
  242. {
  243. $locale = $this->localeResolver->getLocale();
  244. $formattedOpenings = [];
  245. if (isset($openingHours['general'])) {
  246. $formattedOpenings = $this->formatGeneralOpenings($openingHours['general'], $locale);
  247. }
  248. $formattedSpecifics = [
  249. 'openings' => [],
  250. 'closures' => [],
  251. ];
  252. if (isset($openingHours['specific']) && isset($openingHours['specific']['openings'])) {
  253. $formattedSpecifics['openings'] = $this->formatSpecifics($openingHours['specific']['openings'], $locale);
  254. }
  255. if (isset($openingHours['specific']) && isset($openingHours['specific']['closures'])) {
  256. $formattedSpecifics['closures'] = $this->formatSpecifics($openingHours['specific']['closures'], $locale);
  257. }
  258. $formatted = [
  259. 'general' => $formattedOpenings,
  260. 'specific' => $formattedSpecifics,
  261. ];
  262. return $formatted;
  263. }
  264. }