| /* | |
| * Copyright (C) 2022 Apple Inc. All rights reserved. | |
| * | |
| * Redistribution and use in source and binary forms, with or without | |
| * modification, are permitted provided that the following conditions | |
| * are met: | |
| * 1. Redistributions of source code must retain the above copyright | |
| * notice, this list of conditions and the following disclaimer. | |
| * 2. Redistributions in binary form must reproduce the above copyright | |
| * notice, this list of conditions and the following disclaimer in the | |
| * documentation and/or other materials provided with the distribution. | |
| * | |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY | |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR | |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| */ | |
| "use strict"; | |
| function createLists(store, count) { | |
| for (let i = 0; i < count; i++) { | |
| store.lists.push(new ObservableReminderList(store, listName(i))); | |
| } | |
| } | |
| function createReminders(store, count) { | |
| store.lists.forEach((list, listIndex) => { | |
| for (let i = 0; i < count; i++) { | |
| const text = nouns[((listIndex * 10) + i) % nouns.length]; | |
| list.reminders.push(new ObservableReminder(list, text)); | |
| } | |
| }); | |
| } | |
| function randomItem(array) { | |
| return array[array.length * Math.random() | 0]; | |
| } | |
| function listName(index) { | |
| const date = new Date(); | |
| date.setDate(date.getDate() + index); | |
| return date.toDateString(); | |
| } | |
| const nouns = ["magazine", "passion", "statement", "extent", "judgment", "setting", "piano", "control", "employer", "speech", "presence", "indication", "philosophy", "worker", "supermarket", "atmosphere", "actor", "chest", "garbage", "feedback", "pizza", "speaker", "selection", "effort", "math", "gate", "profession", "recognition", "software", "coffee", "reading", "hotel", "concept", "poem", "population", "property", "office", "phone", "suggestion", "theory", "drawer", "death", "cigarette", "alcohol", "marketing", "session", "cheek", "establishment", "family", "revenue", "location", "mode", "awareness", "preference", "consequence", "cookie", "loss", "poetry", "cabinet", "difference", "story", "penalty", "king", "opinion", "climate", "imagination", "criticism", "meal", "desk", "dealer", "introduction", "physics", "customer", "child", "problem", "medicine", "church", "unit", "tongue", "communication", "storage", "stranger", "presentation", "discussion", "excitement", "role", "ladder", "estate", "town", "proposal", "employee", "administration", "variety", "television", "president", "inspector", "negotiation", "height", "comparison", "decision"]; | |
| const adjectives = ["fast", "cheerful", "hungry", "fantastic", "squealing", "critical", "yielding", "abashed", "shocking", "quickest"]; | |
| function addRandomTags(store) { | |
| forEachReminder(store, reminder => { | |
| if (Math.random() < 0.5) { | |
| reminder.tags.add(randomItem(adjectives)); | |
| } | |
| if (Math.random() < 0.5) { | |
| reminder.tags.add(randomItem(adjectives)); | |
| } | |
| }); | |
| } | |
| function completeSomeReminders(store) { | |
| forEachReminder(store, reminder => { | |
| reminder.isCompleted = Math.random() < 0.5; | |
| }); | |
| } | |
| function sortRemindersByCompleted(store) { | |
| store.lists.forEach(list => { | |
| list.reminders.sort((a, b) => b.isCompleted - a.isCompleted); | |
| }); | |
| } | |
| function pushBackRemindersWithTag(store, tag) { | |
| for (let i = store.lists.length - 2; i >= 0; i--) { | |
| const list = store.lists[i]; | |
| const nextList = store.lists[i + 1]; | |
| for (let j = list.reminders.length - 1; j >= 0; j--) { | |
| const reminder = list.reminders[j]; | |
| if (reminder.tags.has(tag)) { | |
| reminder.moveToAnotherList(nextList); | |
| } | |
| } | |
| } | |
| } | |
| function removeRemindersWithTag(store, tag) { | |
| store.lists.forEach(list => { | |
| list.reminders = list.reminders.filter(reminder => !reminder.tags.has(tag)); | |
| }); | |
| } | |
| function removeCompletedReminders(store) { | |
| store.lists.forEach(list => { | |
| list.reminders = list.reminders.filter(reminder => !reminder.isCompleted); | |
| }); | |
| } | |
| function removeFirstAndLastRemindersFromEachList(store) { | |
| store.lists.forEach(list => { | |
| list.reminders.shift(); | |
| list.reminders.pop(); | |
| }); | |
| } | |
| function removeEmptyLists(store) { | |
| for (let i = store.lists.length - 1; i >= 0; i--) { | |
| const list = store.lists[i]; | |
| if (list.isEmpty) { | |
| store.lists.splice(i, 1); | |
| } | |
| } | |
| } | |
| function forEachReminder(store, callback) { | |
| store.lists.forEach(list => { | |
| list.reminders.forEach(callback); | |
| }); | |
| } | |
| function renderTags(entries) { | |
| return entries.map(([tag, count]) => `#${tag} (${count})`).join(", ") || "<none>"; | |
| } | |
| class ReminderStore { | |
| constructor() { | |
| this.lists = []; | |
| this.allTimeCompletedTags = new Map; | |
| } | |
| get allTags() { | |
| const map = new Map; | |
| forEachReminder(this, reminder => { | |
| reminder.tags.forEach(tag => { | |
| const oldCount = map.get(tag) ?? 0; | |
| map.set(tag, oldCount + 1); | |
| }); | |
| }); | |
| return map; | |
| } | |
| get completedRemidersCount() { | |
| return this.lists.reduce((count, list) => count + list.completedRemidersCount, 0); | |
| } | |
| get remindersCount() { | |
| return this.lists.reduce((count, list) => count + list.reminders.length, 0); | |
| } | |
| get popularTags() { | |
| return this.popularTagsWithCount.map(e => e[0]); | |
| } | |
| get popularTagsWithCount() { | |
| return [...this.allTags].sort((a, b) => b[1] - a[1]).slice(0, 5); | |
| } | |
| render() { | |
| return [ | |
| this.lists.map(list => list.render()).join("\n") || "<empty>", | |
| "-".repeat(31), | |
| `completed: ${this.completedRemidersCount} / ${this.remindersCount}`, | |
| "-".repeat(31), | |
| `current popular tags: ${renderTags(this.popularTagsWithCount)}`, | |
| `all-time completed tags: ${renderTags([...this.allTimeCompletedTags])}`, | |
| ].join("\n"); | |
| } | |
| } | |
| class ReminderList { | |
| constructor(store, name) { | |
| this._store = store; | |
| this.name = name; | |
| this.reminders = []; | |
| } | |
| get store() { | |
| return this._store; | |
| } | |
| get indexInStore() { | |
| return this._store.lists.indexOf(this); | |
| } | |
| get isEmpty() { | |
| return !this.reminders.length; | |
| } | |
| get completedRemidersCount() { | |
| return this.reminders.filter(reminder => reminder.isCompleted).length; | |
| } | |
| render() { | |
| const content = this.isEmpty ? "<empty>" : this.reminders.map(reminder => reminder.render()).join("\n"); | |
| return `--- ${this.name} (${this.completedRemidersCount} / ${this.reminders.length}) ---\n${content}`; | |
| } | |
| } | |
| class Reminder { | |
| constructor(list, text) { | |
| this._list = list; | |
| this.text = text; | |
| this.tags = new Set(); | |
| this.isCompleted = false; | |
| } | |
| get list() { | |
| return this._list; | |
| } | |
| get indexInList() { | |
| return this._list.reminders.indexOf(this); | |
| } | |
| _isCompletedChanged(isCompleted) { | |
| if (isCompleted) { | |
| const tagsMap = this._list.store.allTimeCompletedTags; | |
| this.tags.forEach(tag => { | |
| const oldCount = tagsMap.get(tag) ?? 0; | |
| tagsMap.set(tag, oldCount + 1); | |
| }); | |
| } | |
| } | |
| moveToAnotherList(newList) { | |
| this._list.reminders.splice(this.indexInList, 1); | |
| this._list = newList; | |
| newList.reminders.unshift(this); | |
| } | |
| render() { | |
| const tags = this.tags.size ? ` (${Array.from(this.tags, tag => `#${tag}`).join(" ")})` : ""; | |
| return `[${this.isCompleted ? "x" : " "}] ${this.text}${tags}`; | |
| } | |
| } | |
| async function test(store, render) { | |
| await render(() => createLists(store, 10)); | |
| await render(() => createReminders(store, 10)); | |
| await render(() => addRandomTags(store)); | |
| const { popularTags } = store; | |
| await render(() => pushBackRemindersWithTag(store, popularTags[0])); | |
| await render(() => removeRemindersWithTag(store, popularTags[2])); | |
| await render(() => removeFirstAndLastRemindersFromEachList(store)); | |
| await render(() => completeSomeReminders(store)); | |
| await render(() => sortRemindersByCompleted(store)); | |
| await render(() => removeCompletedReminders(store)); | |
| await render(() => removeEmptyLists(store)); | |
| } |