diff --git a/01 - JavaScript Drum Kit/index.js b/01 - JavaScript Drum Kit/index.js new file mode 100644 index 0000000000..bb775a0237 --- /dev/null +++ b/01 - JavaScript Drum Kit/index.js @@ -0,0 +1,25 @@ +const ks = [...document.querySelectorAll('.key')]; + +// add event to k +ks.forEach((k) => { + k.addEventListener('transitionend', (e) => { + if (e.propertyName !== 'transform') return; + e.target.classList.remove('playing'); + console.log(e); + }); +}); + +window.addEventListener('keydown', (e) => playSound(e.keyCode)); + +function playSound(code) { + const btn = document.querySelector(`div[data-key='${code}']`); + const audio = document.querySelector(`audio[data-key='${code}']`); + if (!audio) return; + + btn.classList.add('playing'); + audio.currentTime = 0; + audio.play(); + + // setTimeout(() => btn.classList.remove('playing'), 500); + // console.log(code, btn, audio); +} diff --git a/01 - JavaScript Drum Kit/index0.html b/01 - JavaScript Drum Kit/index0.html new file mode 100644 index 0000000000..421f2353e9 --- /dev/null +++ b/01 - JavaScript Drum Kit/index0.html @@ -0,0 +1,60 @@ + + + + + JS Drum Kit + + + +
+
+ A + clap +
+
+ S + hihat +
+
+ D + kick +
+
+ F + openhat +
+
+ G + boom +
+
+ H + ride +
+
+ J + snare +
+
+ K + tom +
+
+ L + tink +
+
+ + + + + + + + + + + + + + diff --git a/02 - JS and CSS Clock/index.js b/02 - JS and CSS Clock/index.js new file mode 100644 index 0000000000..eef455e456 --- /dev/null +++ b/02 - JS and CSS Clock/index.js @@ -0,0 +1,23 @@ +const data = { + digitclock: document.querySelector('#digitclock'), + shand: document.querySelector('.second-hand'), + mhand: document.querySelector('.min-hand'), + hhand: document.querySelector('.hour-hand'), +}; + +function showtime({ digitclock, shand, mhand, hhand }) { + const t = new Date(), + sdeg = t.getSeconds() * (360 / 60) + 90, + mdeg = + t.getMinutes() * (360 / 60) + 90 + ((t.getSeconds() / 60) * 360) / 60, + hdeg = t.getHours() * (360 / 12) + 90 + t.getMinutes() * 0.5; + + digitclock.textContent = t.toLocaleTimeString(); + shand.style.transform = `rotate(${sdeg}deg)`; + mhand.style.transform = `rotate(${mdeg}deg)`; + hhand.style.transform = `rotate(${hdeg}deg)`; +} + +const iPnt = setInterval(() => showtime(data), 1000); + +// showtime(data); diff --git a/02 - JS and CSS Clock/index0.html b/02 - JS and CSS Clock/index0.html new file mode 100644 index 0000000000..17d9b52c4e --- /dev/null +++ b/02 - JS and CSS Clock/index0.html @@ -0,0 +1,83 @@ + + + + + JS + CSS Clock + + +
+
+
+
+
+
+
+
+ +
time
+ + + + + + diff --git a/03 - CSS Variables/index0.html b/03 - CSS Variables/index0.html new file mode 100644 index 0000000000..83eeddd553 --- /dev/null +++ b/03 - CSS Variables/index0.html @@ -0,0 +1,91 @@ + + + + + Scoped CSS Variables and JS + + +

Update CSS Variables with JS

+ +
+ + + + + + + + +
+ + + + + + + + diff --git a/05 - Flex Panel Gallery/index0.html b/05 - Flex Panel Gallery/index0.html new file mode 100644 index 0000000000..89e5a7776c --- /dev/null +++ b/05 - Flex Panel Gallery/index0.html @@ -0,0 +1,133 @@ + + + + + Flex Panels 💪 + + + + +
+
+

Hey

+

Let's

+

Dance

+
+
+

Give

+

Take

+

Receive

+
+
+

Experience

+

It

+

Today

+
+
+

Give

+

All

+

You can

+
+
+

Life

+

In

+

Motion

+
+
+ + + + diff --git a/06 - Type Ahead/app/app.js b/06 - Type Ahead/app/app.js new file mode 100644 index 0000000000..8b2989fe85 --- /dev/null +++ b/06 - Type Ahead/app/app.js @@ -0,0 +1,185 @@ +import { Suggestions } from './components.js'; +// Utilities Start ==================== +const pr = console.log; +// Utilities End ==================== + +// App Start ==================== +// +export class EventRegistry { + constructor() { + this.map = new Map(); + } + + fire(event) { + for (const cb of this.map.get(event.name) || []) { + cb(event); + } + } + + listen(eventName, cb) { + const cbs = this.map.get(eventName) || []; + cbs.push(cb); + this.map.set(eventName, cbs); + } + unListen(eventName, cb) { + const cbs = this.map.get(eventName) || []; + this.map.set( + eventName, + cbs.filter((x) => x != cb), + ); + } +} + +export class Place { + constructor(city, st) { + this.city = city; + this.st = st; + } +} + +export const App = new EventRegistry(); +App.filter = ''; + +App.setupDispatch = () => { + pr('App.setupDispatch App:', App); + App.db.onmessage = (e) => { + const { op, error } = e.data; + pr(op, 'data:', e.data); + switch (op) { + case 'addDone': + if (!error) { + App.getPlaces(App.filter); + App.fire({ name: 'addCity success', data: e.data }); + } else { + App.fire({ name: 'addCity error', error: e.data.error.message }); + } + break; + case 'delDone': + if (!error) { + App.getPlaces(App.filter); + } else { + App.fire({ name: 'delCity error', error: e.data.error.message }); + } + break; + case 'getDone': + App.fire({ name: 'placesChange', data: e.data }); + break; + default: + pr('App.onmessage.default'); + } + }; +}; + +App.getPlaces = (pattern) => { + const msg = { op: 'get', path: 'place', pattern: pattern }; + pr('App.getPlaces msg:', msg); + App.db.postMessage(msg); +}; + +App.filterChange = (pattern) => { + App.filter = pattern; + App.getPlaces(pattern); + pr('App.filterChange pattern:', pattern); + App.fire({ name: 'filterChange', filter: pattern }); +}; + +App.addCity = (str) => { + const [city, st] = str + .toLowerCase() + .split(',') + .map((x) => x.trim()); + if (city && st) { + App.db.postMessage({ op: 'add', path: 'place', obj: new Place(city, st) }); + } +}; + +App.delCity = (city, st) => { + App.db.postMessage({ op: 'del', path: 'place', obj: new Place(city, st) }); +}; + +App.init = () => { + App.db = new Worker('store-worker.js'); + App.setupDispatch(); + + const suggestionsDiv = document.querySelector('.suggestions'); + App.listen('placesChange', ({ data }) => { + const places = data.data; + pr('suggestionsDiv, placesChange, data:', data, places); + const component = Suggestions({ + places: places, + onDel: (city, st) => App.delCity(city, st), + onEdit: (city, st) => App.editCity(city, st), + }); + render(suggestionsDiv, component); + }); + + // filters + // TODO: throttle keyup events for 200ms // + document.querySelectorAll('.filter').forEach((el) => { + el.addEventListener('keyup', (e) => App.filterChange(e.target.value)); + App.listen('filterChange', ({ filter }) => { + if (el.value !== filter) el.value = filter; + }); + }); + + // add city + const addInp = document.querySelector('.add'); + addInp.addEventListener('change', (e) => App.addCity(e.target.value)); + App.listen('addCity success', () => { + addInp.value = ''; + }); + App.listen('addCity error', (e) => { + addInp.value = e.error; + }); + + // const { place, err } = App.addCity(e.target.value); + // if (err) { + // pr('Error', err); + // return; + // } + // const event = { name: 'PlacesChange', place: place }; + // App.eReg.fireEvent(event); + // e.target.value = ''; + // }); +}; + +// Element, Element -> DOM Effect +function render(root, component) { + root.replaceChildren(component); +} + +// App End ==================== + +// App Run ==================== +App.init(); + +// e// // str, str -> err: str +// delCity(city, st) { +// pr(city, st); +// this._places.delete(this.key(city, st)); +// this.eReg.fireEvent({ +// name: 'placesChange', +// city: city, +// st: st, +// details: 'delete', +// }); +// } + +// // str, str -> err: str +// editCity(city, st) { +// pr(city, st); +// } +// } + +// export const app = new App(new Worker('store-worker.js')); +// App End ==================== + +// store-worker Start ==================== +// const store = new Worker('store-worker.js'); + +// window.onload = (e) => { +// pr('onload', e); +// store.postMessage({ op: 'get', path: 'place', pattern: '' }); +// }; + +// store-worker End ==================== diff --git a/06 - Type Ahead/app/components.js b/06 - Type Ahead/app/components.js new file mode 100644 index 0000000000..784bc414e3 --- /dev/null +++ b/06 - Type Ahead/app/components.js @@ -0,0 +1,76 @@ +// Utilitis +const pr = console.table; +const Element = (tag) => document.createElement(tag); + +// {places, ...[onChangeFn]} -> Element('ul') +export function Suggestions({ places, onDel, onEdit }) { + pr('Suggestions places:', places); + const lst = places.map(({ city, st }) => { + const li = Element('li'), + c = Element('span'), + s = Element('span'), + del = Element('button'), + ed = Element('button'), + sbtn = Element('span'); + c.append(city); + s.append(st); + del.append('Del'); + del.addEventListener('click', () => { + onDel(city, st); + }); + ed.append('Edit'); + ed.addEventListener('click', () => { + onEdit(city, st); + }); + sbtn.append(ed, del); + li.append(c, s, sbtn); + return li; + }); + const ul = Element('ul'); + ul.append(...lst); + return ul; +} +// ========== + +// const suggestions = document.querySelector('.suggestions'); +// // str, Element -> +// function showSuggestions(pattern, rootEl) { +// const ps = App.places(pattern), +// lis = placesList({ +// places: ps, +// onDel: (city, st) => App.delCity(city, st), +// onEdit: (city, st) => App.editCity(city, st), +// }); +// rootEl.replaceChildren(...lis); +// } + +// App.eReg.listen('SearchChange', ({ pattern }) => +// showSuggestions(pattern, suggestions), +// ); + +// App.eReg.listen('PlacesChange', () => { +// const pattern = search.value; +// showSuggestions(pattern, suggestions); +// }); + +// showSuggestions('', suggestions); +// ========== + +// const search = document.querySelector('.search'); +// search.addEventListener('keyup', (e) => { +// const event = { name: 'SearchChange', pattern: e.target.value }; +// App.eReg.fireEvent(event); +// }); +// // ========== + +// const add = document.querySelector('.add'); +// add.addEventListener('change', (e) => { +// const { place, err } = App.addCity(e.target.value); +// if (err) { +// pr('Error', err); +// return; +// } +// const event = { name: 'PlacesChange', place: place }; +// App.eReg.fireEvent(event); +// e.target.value = ''; +// }); diff --git a/06 - Type Ahead/app/index.js b/06 - Type Ahead/app/index.js new file mode 100644 index 0000000000..24187679de --- /dev/null +++ b/06 - Type Ahead/app/index.js @@ -0,0 +1,164 @@ +// Utilitis +const pr = console.table; +const Element = (tag) => document.createElement(tag); + +class EventRegistry { + constructor() { + this.map = new Map(); + } + + fireEvent(event) { + for (const cb of this.map.get(event.name) || []) { + cb(event); + } + } + + listen(eventName, cb) { + const cbs = this.map.get(eventName) || []; + cbs.push(cb); + this.map.set(eventName, cbs); + } + unListen(eventName, cb) { + const cbs = this.map.get(eventName) || []; + this.map.set( + eventName, + cbs.filter((x) => x != cb), + ); + } +} + +const App = { + eReg: new EventRegistry(), + _places: new Map(), + + init() { + // init places + const _cs = ['Madera,ca', 'Fresno, CA', 'boulder, co', 'austin,tx']; + for (const s of _cs) { + this.addCity(s); + } + }, + + // str -> [place] + places(pattern) { + const r = new RegExp(pattern, 'gi'), + res = []; + this._places.forEach((v, k) => { + if (k.match(r)) res.push(v); + }); + return res; + }, + + // str, str -> str + key(city, st) { + return `${city},${st}`; + }, + + // str -> {place: obj, err: str} + addCity(str) { + const [city, st] = str + .toLowerCase() + .split(',') + .map((x) => x.trim()); + const key = this.key(city, st), + place = { city: city, st: st }; + if (this._places.has(key)) return { place: null, err: 'key exists' }; + this._places.set(key, place); + this.eReg.fireEvent({ name: 'PlacesChange', city: city, st: st }); + return { place: place, err: '' }; + }, + + // str, str -> err: str + delCity(city, st) { + pr(city, st); + this._places.delete(this.key(city, st)); + this.eReg.fireEvent({ + name: 'PlacesChange', + city: city, + st: st, + details: 'delete', + }); + }, + + // str, str -> err: str + editCity(city, st) { + pr(city, st); + }, +}; + +App.init(); +// ========== + +// Componets +// +// [places], rootElment -> [Element('li')] +function placesList({ places, onDel, onEdit }) { + const lst = places.map(({ city, st }) => { + const li = Element('li'), + c = Element('span'), + s = Element('span'), + del = Element('button'), + ed = Element('button'), + sbtn = Element('span'); + c.append(city); + s.append(st); + del.append('Del'); + del.addEventListener('click', () => { + onDel(city, st); + }); + ed.append('Edit'); + ed.addEventListener('click', () => { + onEdit(city, st); + }); + sbtn.append(ed, del); + li.append(c, s, sbtn); + return li; + }); + return lst; +} +// ========== + +const suggestions = document.querySelector('.suggestions'); +// str, Element -> +function showSuggestions(pattern, rootEl) { + const ps = App.places(pattern), + lis = placesList({ + places: ps, + onDel: (city, st) => App.delCity(city, st), + onEdit: (city, st) => App.editCity(city, st), + }); + rootEl.replaceChildren(...lis); +} + +App.eReg.listen('SearchChange', ({ pattern }) => + showSuggestions(pattern, suggestions), +); + +App.eReg.listen('PlacesChange', () => { + const pattern = search.value; + showSuggestions(pattern, suggestions); +}); + +showSuggestions('', suggestions); +// ========== + +const search = document.querySelector('.search'); +search.addEventListener('keyup', (e) => { + const event = { name: 'SearchChange', pattern: e.target.value }; + App.eReg.fireEvent(event); +}); +// ========== + +const add = document.querySelector('.add'); +add.addEventListener('change', (e) => { + const { place, err } = App.addCity(e.target.value); + if (err) { + pr('Error', err); + return; + } + const event = { name: 'PlacesChange', place: place }; + App.eReg.fireEvent(event); + e.target.value = ''; +}); + +// END Componets =============== diff --git a/06 - Type Ahead/app/main.html b/06 - Type Ahead/app/main.html new file mode 100644 index 0000000000..43ea172970 --- /dev/null +++ b/06 - Type Ahead/app/main.html @@ -0,0 +1,36 @@ + + + + + Type Ahead 👀 + + + +
+
+ + +
+
+ + +
+
+ +
+
+ + + diff --git a/06 - Type Ahead/app/mocha b/06 - Type Ahead/app/mocha new file mode 120000 index 0000000000..67d7fb6daa --- /dev/null +++ b/06 - Type Ahead/app/mocha @@ -0,0 +1 @@ +/Users/v/code/learning/js/mocha \ No newline at end of file diff --git a/06 - Type Ahead/app/store-worker.js b/06 - Type Ahead/app/store-worker.js new file mode 100644 index 0000000000..5a930ce1e2 --- /dev/null +++ b/06 - Type Ahead/app/store-worker.js @@ -0,0 +1,130 @@ +// import { Place } from './app.js'; +const pr = console.log; +let db; + +const connection = indexedDB.open('cities', 1); + +connection.onerror = (e) => pr('why not allow to use indexedDB?'); + +connection.onupgradeneeded = (e) => { + pr('in connection.onupgradeneeded'); + db = e.target.result; + db.onerror = (e) => console.error('Error loading db'); + + // create objectStore + const objectStore = db.createObjectStore('place', { keyPath: 'key' }); + // define data items + // objectStore.createIndex('zip', 'zip', {unique: false}) + + //populate with initial data + objectStore.transaction.oncomplete = (e) => { + pr('onupgradeneeded oncomplete, e', e); + add(db, { + op: 'add', + path: 'place', + obj: { city: 'example city', st: 'ny' }, + }); + }; +}; + +connection.onsuccess = (e) => { + pr('in connection.onsuccess'); + db = e.target.result; + // setupDispatch(db); + // get all places + get(db, { + op: 'get', + path: 'place', + pattern: '', + }); +}; + +// setupDispatch +onmessage = (event) => { + const { op } = event.data; + switch (op) { + case 'get': + get(db, event.data); + break; + + case 'add': + add(db, event.data); + break; + + case 'del': + del(db, event.data); + break; + default: + pr('worker.setupDispatch.default', event.data); + } +}; + +function key({ city, st }) { + return `${city},${st}`; +} + +function add(db, data) { + const { path, obj } = data; + if (path === 'place') { + obj.key = key(obj); + } + + const transaction = db.transaction([path], 'readwrite'); + const objectStore = transaction.objectStore(path); + const req = objectStore.add(obj); + + req.onsuccess = (e) => { + const res = { data: e.target.result, op: 'addDone', error: '' }; + postMessage(res); + }; + req.onerror = (e) => { + const res = { data: e.target.result, op: 'addDone', error: e.target.error }; + postMessage(res); + }; +} + +function get(db, data) { + const { path, pattern } = data; + const r = new RegExp(pattern, 'gi'), + trans = db.transaction([path]), + items = []; + + objectStore = trans.objectStore(path); + + objectStore.openCursor().onsuccess = (e) => { + const cursor = e.target.result; + if (!cursor) { + pr('lookup is done'); + const res = { op: 'getDone', data: items, pattern: pattern, error: '' }; + postMessage(res); + return; + } + const item = cursor.value; + if (item.key.match(r)) items.push(item); + cursor.continue(); + }; +} + +function del(db, data) { + const { path, obj } = data; + if (path === 'place') { + obj.key = key(obj); + } + + const transaction = db.transaction([path], 'readwrite'); + const objectStore = transaction.objectStore(path); + const req = objectStore.delete(obj.key); + + req.onsuccess = (e) => { + const res = { data: e.target.result, op: 'delDone', error: '' }; + postMessage(res); + }; + req.onerror = (e) => { + const res = { + data: e.target.result, + op: 'delDone', + error: e.target.error, + }; + postMessage(res); + }; +} diff --git a/06 - Type Ahead/app/style.css b/06 - Type Ahead/app/style.css new file mode 100644 index 0000000000..2e62f4e331 --- /dev/null +++ b/06 - Type Ahead/app/style.css @@ -0,0 +1,84 @@ +html { + box-sizing: border-box; + background: #ffc600; + font-family: 'helvetica neue'; + font-size: 20px; + font-weight: 200; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +div.utils { + border: 2px dotted black; +} +.utils input { + width: 30%; + height: 20px; +} + +input { + width: 100%; + padding: 20px; +} + +.search-form { + max-width: 400px; + margin: 50px auto; +} + +input.search { + margin: 0; + text-align: center; + outline: 0; + border: 10px solid #f7f7f7; + width: 120%; + left: -10%; + position: relative; + top: 10px; + z-index: 2; + border-radius: 5px; + font-size: 40px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.12), inset 0 0 2px rgba(0, 0, 0, 0.19); +} + +.suggestions { + margin: 0; + padding: 0; + position: relative; + /*perspective: 20px;*/ +} + +.suggestions li { + background: white; + list-style: none; + border-bottom: 1px solid #d8d8d8; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.14); + margin: 0; + padding: 20px; + transition: background 0.2s; + display: flex; + justify-content: space-between; + text-transform: capitalize; +} + +.suggestions li:nth-child(even) { + transform: perspective(100px) rotateX(3deg) translateY(2px) scale(1.001); + background: linear-gradient(to bottom, #ffffff 0%, #efefef 100%); +} + +.suggestions li:nth-child(odd) { + transform: perspective(100px) rotateX(-3deg) translateY(3px); + background: linear-gradient(to top, #ffffff 0%, #efefef 100%); +} + +span.population { + font-size: 15px; +} + +.hl { + background: #ffc600; +} diff --git a/06 - Type Ahead/app/t.spec.js b/06 - Type Ahead/app/t.spec.js new file mode 100644 index 0000000000..6ea37cea6f --- /dev/null +++ b/06 - Type Ahead/app/t.spec.js @@ -0,0 +1,82 @@ +const assert = chai.assert; +import { Place, App, EventRegistry } from './app.js'; + +const pr = console.log; + +describe('function', () => { + it('desctructure arguments', () => { + function add({ a, b }) { + return a + b; + } + const props = { a: 1, b: 3 }; + assert.equal(4, add(props)); + }); +}); + +describe('class', () => { + it('desctructure class obj', () => { + const place = new Place('a', 'ca'); + const { city, st } = place; + assert.equal('a', city); + }); + it('extends', () => { + class A { + constructor(x) { + this.x = x; + } + add(y) { + return this.x + y; + } + sub(y) { + return this.x - y; + } + } + class B extends A { + constructor(x) { + super(x); + } + add(y) { + return this.x * y; + } + } + const a = new A(1), + b = new B(1); + + assert.equal(a.add(1), 2); + assert.equal(a.sub(1), 0); + assert.equal(b.sub(1), 0); + assert.equal(b.add(1), 1); + }); +}); + +describe('App', () => { + it('extends EventRegistry', () => { + class Reg extends EventRegistry { + constructor(x) { + super(); + this.x = x; + this.listen('change a', (e) => { + // pr('in Reg.constructor, e', e); + }); + } + f(x) { + this.fire({ name: 'change a', data: x }); + } + } + + const reg = new Reg(1), + a = { a: 1, b: 2 }, + b = { a: 1, b: 2 }; + + reg.listen('change a', (e) => { + // pr('in describe App, e', e); + b.a = e.data + b.a; + }); + assert.deepEqual({ a: 1, b: 2 }, b); + reg.fire({ name: 'change a', data: 1 }); + assert.deepEqual({ a: 2, b: 2 }, b); + + reg.f(2); + assert.deepEqual({ a: 4, b: 2 }, b); + }); +}); diff --git a/06 - Type Ahead/app/tests.html b/06 - Type Ahead/app/tests.html new file mode 100644 index 0000000000..0766a8b57c --- /dev/null +++ b/06 - Type Ahead/app/tests.html @@ -0,0 +1,30 @@ + + + + + Mocha Tests + + + + + +
+ + + + + + + + + + + + + + diff --git a/06 - Type Ahead/cities.json b/06 - Type Ahead/cities.json new file mode 100644 index 0000000000..259103a7b8 --- /dev/null +++ b/06 - Type Ahead/cities.json @@ -0,0 +1 @@ +[{ "1": 2 }, { "1": 2 }] diff --git a/06 - Type Ahead/events.html b/06 - Type Ahead/events.html new file mode 100644 index 0000000000..ca531685c7 --- /dev/null +++ b/06 - Type Ahead/events.html @@ -0,0 +1,21 @@ + + + + + Type Ahead 👀 + + + +
+
+ + +
+ +
+ + + diff --git a/06 - Type Ahead/events.js b/06 - Type Ahead/events.js new file mode 100644 index 0000000000..24187679de --- /dev/null +++ b/06 - Type Ahead/events.js @@ -0,0 +1,164 @@ +// Utilitis +const pr = console.table; +const Element = (tag) => document.createElement(tag); + +class EventRegistry { + constructor() { + this.map = new Map(); + } + + fireEvent(event) { + for (const cb of this.map.get(event.name) || []) { + cb(event); + } + } + + listen(eventName, cb) { + const cbs = this.map.get(eventName) || []; + cbs.push(cb); + this.map.set(eventName, cbs); + } + unListen(eventName, cb) { + const cbs = this.map.get(eventName) || []; + this.map.set( + eventName, + cbs.filter((x) => x != cb), + ); + } +} + +const App = { + eReg: new EventRegistry(), + _places: new Map(), + + init() { + // init places + const _cs = ['Madera,ca', 'Fresno, CA', 'boulder, co', 'austin,tx']; + for (const s of _cs) { + this.addCity(s); + } + }, + + // str -> [place] + places(pattern) { + const r = new RegExp(pattern, 'gi'), + res = []; + this._places.forEach((v, k) => { + if (k.match(r)) res.push(v); + }); + return res; + }, + + // str, str -> str + key(city, st) { + return `${city},${st}`; + }, + + // str -> {place: obj, err: str} + addCity(str) { + const [city, st] = str + .toLowerCase() + .split(',') + .map((x) => x.trim()); + const key = this.key(city, st), + place = { city: city, st: st }; + if (this._places.has(key)) return { place: null, err: 'key exists' }; + this._places.set(key, place); + this.eReg.fireEvent({ name: 'PlacesChange', city: city, st: st }); + return { place: place, err: '' }; + }, + + // str, str -> err: str + delCity(city, st) { + pr(city, st); + this._places.delete(this.key(city, st)); + this.eReg.fireEvent({ + name: 'PlacesChange', + city: city, + st: st, + details: 'delete', + }); + }, + + // str, str -> err: str + editCity(city, st) { + pr(city, st); + }, +}; + +App.init(); +// ========== + +// Componets +// +// [places], rootElment -> [Element('li')] +function placesList({ places, onDel, onEdit }) { + const lst = places.map(({ city, st }) => { + const li = Element('li'), + c = Element('span'), + s = Element('span'), + del = Element('button'), + ed = Element('button'), + sbtn = Element('span'); + c.append(city); + s.append(st); + del.append('Del'); + del.addEventListener('click', () => { + onDel(city, st); + }); + ed.append('Edit'); + ed.addEventListener('click', () => { + onEdit(city, st); + }); + sbtn.append(ed, del); + li.append(c, s, sbtn); + return li; + }); + return lst; +} +// ========== + +const suggestions = document.querySelector('.suggestions'); +// str, Element -> +function showSuggestions(pattern, rootEl) { + const ps = App.places(pattern), + lis = placesList({ + places: ps, + onDel: (city, st) => App.delCity(city, st), + onEdit: (city, st) => App.editCity(city, st), + }); + rootEl.replaceChildren(...lis); +} + +App.eReg.listen('SearchChange', ({ pattern }) => + showSuggestions(pattern, suggestions), +); + +App.eReg.listen('PlacesChange', () => { + const pattern = search.value; + showSuggestions(pattern, suggestions); +}); + +showSuggestions('', suggestions); +// ========== + +const search = document.querySelector('.search'); +search.addEventListener('keyup', (e) => { + const event = { name: 'SearchChange', pattern: e.target.value }; + App.eReg.fireEvent(event); +}); +// ========== + +const add = document.querySelector('.add'); +add.addEventListener('change', (e) => { + const { place, err } = App.addCity(e.target.value); + if (err) { + pr('Error', err); + return; + } + const event = { name: 'PlacesChange', place: place }; + App.eReg.fireEvent(event); + e.target.value = ''; +}); + +// END Componets =============== diff --git a/06 - Type Ahead/worker/app.js b/06 - Type Ahead/worker/app.js new file mode 100644 index 0000000000..54b2904e56 --- /dev/null +++ b/06 - Type Ahead/worker/app.js @@ -0,0 +1,6 @@ +import { Lst } from './lst.js'; + +const pr = console.log; + +pr('hi'); +pr(Lst.map((x) => x.name + x.val)); diff --git a/06 - Type Ahead/worker/index.html b/06 - Type Ahead/worker/index.html new file mode 100644 index 0000000000..20e0408efc --- /dev/null +++ b/06 - Type Ahead/worker/index.html @@ -0,0 +1,21 @@ + + + + + Type Ahead 👀 + + + +
+
+ + +
+ +
+ + + diff --git a/06 - Type Ahead/worker/lst.js b/06 - Type Ahead/worker/lst.js new file mode 100644 index 0000000000..0917c41274 --- /dev/null +++ b/06 - Type Ahead/worker/lst.js @@ -0,0 +1,5 @@ +export const Lst = [ + { name: 'a', val: 'aa' }, + { name: 'b', val: 'bb' }, + { name: 'c', val: 'cc' }, +]; diff --git a/27 - Click and Drag/index-FINISHED.html b/27 - Click and Drag/index-FINISHED.html index 52eb86628c..aefeedcddc 100644 --- a/27 - Click and Drag/index-FINISHED.html +++ b/27 - Click and Drag/index-FINISHED.html @@ -1,71 +1,69 @@ - - - Click and Drag - - - -
-
01
-
02
-
03
-
04
-
05
-
06
-
07
-
08
-
09
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
+ + + Click and Drag + + + +
+
01
+
02
+
03
+
04
+
05
+
06
+
07
+
08
+
09
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
- + slider.addEventListener('mouseup', () => { + isDown = false; + slider.classList.remove('active'); + }); + slider.addEventListener('mousemove', (e) => { + if (!isDown) return; // stop the fn from running + e.preventDefault(); + const x = e.pageX - slider.offsetLeft; + const walk = (x - startX) * 3; + slider.scrollLeft = scrollLeft - walk; + }); + diff --git a/data/cities.json b/data/cities.json new file mode 100644 index 0000000000..259103a7b8 --- /dev/null +++ b/data/cities.json @@ -0,0 +1 @@ +[{ "1": 2 }, { "1": 2 }] diff --git a/lab1/PT_Part2_Music_Generation.ipynb b/lab1/PT_Part2_Music_Generation.ipynb new file mode 100644 index 0000000000..7c9722391d --- /dev/null +++ b/lab1/PT_Part2_Music_Generation.ipynb @@ -0,0 +1,1438 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uoJsVjtCMunI" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " Visit MIT Deep Learning\n", + " Run in Google Colab\n", + " View Source on GitHub
\n", + "\n", + "# Copyright Information" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bUik05YqMyCH" + }, + "outputs": [], + "source": [ + "# Copyright 2025 MIT Introduction to Deep Learning. All Rights Reserved.\n", + "#\n", + "# Licensed under the MIT License. You may not use this file except in compliance\n", + "# with the License. Use and/or modification of this code outside of MIT Introduction\n", + "# to Deep Learning must reference:\n", + "#\n", + "# © MIT Introduction to Deep Learning\n", + "# http://introtodeeplearning.com\n", + "#" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "O-97SDET3JG-" + }, + "source": [ + "# Lab 1: Intro to PyTorch and Music Generation with RNNs\n", + "\n", + "# Part 2: Music Generation with RNNs\n", + "\n", + "In this portion of the lab, we will explore building a Recurrent Neural Network (RNN) for music generation using PyTorch. We will train a model to learn the patterns in raw sheet music in [ABC notation](https://en.wikipedia.org/wiki/ABC_notation) and then use this model to generate new music." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rsvlBQYCrE4I" + }, + "source": [ + "## 2.1 Dependencies\n", + "First, let's download the course repository, install dependencies, and import the relevant packages we'll need for this lab.\n", + "\n", + "We will be using [Comet ML](https://www.comet.com/docs/v2/) to track our model development and training runs. First, sign up for a Comet account [at this link](https://www.comet.com/signup?utm_source=mit_dl&utm_medium=partner&utm_content=github\n", + ") (you can use your Google or Github account). You will need to generate a new personal API Key, which you can find either in the first 'Get Started with Comet' page, under your account settings, or by pressing the '?' in the top right corner and then 'Quickstart Guide'. Enter this API key as the global variable `COMET_API_KEY`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "riVZCVK65QTH" + }, + "outputs": [], + "source": [ + "!pip install comet_ml > /dev/null 2>&1\n", + "import comet_ml\n", + "# TODO: ENTER YOUR API KEY HERE!! instructions above\n", + "COMET_API_KEY = \"rom6izyKhaALsVmPeV0NjkAmQ\"\n", + "\n", + "# Import PyTorch and other relevant libraries\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "\n", + "# Download and import the MIT Introduction to Deep Learning package\n", + "!pip install mitdeeplearning --quiet\n", + "import mitdeeplearning as mdl\n", + "\n", + "# Import all remaining packages\n", + "import numpy as np\n", + "import os\n", + "import time\n", + "import functools\n", + "from IPython import display as ipythondisplay\n", + "from tqdm import tqdm\n", + "from scipy.io.wavfile import write\n", + "!apt-get install abcmidi timidity > /dev/null 2>&1\n", + "\n", + "\n", + "# Check that we are using a GPU, if not switch runtimes\n", + "# using Runtime > Change Runtime Type > GPU\n", + "assert torch.cuda.is_available(), \"Please enable GPU from runtime settings\"\n", + "assert COMET_API_KEY != \"\", \"Please insert your Comet API Key\"\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_ajvp0No4qDm" + }, + "source": [ + "## 2.2 Dataset\n", + "\n", + "![Let's Dance!](http://33.media.tumblr.com/3d223954ad0a77f4e98a7b87136aa395/tumblr_nlct5lFVbF1qhu7oio1_500.gif)\n", + "\n", + "We've gathered a dataset of thousands of Irish folk songs, represented in the ABC notation. Let's download the dataset and inspect it:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "P7dFnP5q3Jve", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "ab4ed7d0-b691-4988-ac61-b64d7493e69f" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Found 817 songs in text\n", + "\n", + "Example song: \n", + "X:1\n", + "T:Alexander's\n", + "Z: id:dc-hornpipe-1\n", + "M:C|\n", + "L:1/8\n", + "K:D Major\n", + "(3ABc|dAFA DFAd|fdcd FAdf|gfge fefd|(3efe (3dcB A2 (3ABc|!\n", + "dAFA DFAd|fdcd FAdf|gfge fefd|(3efe dc d2:|!\n", + "AG|FAdA FAdA|GBdB GBdB|Acec Acec|dfaf gecA|!\n", + "FAdA FAdA|GBdB GBdB|Aceg fefd|(3efe dc d2:|!\n" + ] + } + ], + "source": [ + "# Download the dataset\n", + "songs = mdl.lab1.load_training_data()\n", + "\n", + "# Print one of the songs to inspect it in greater detail!\n", + "example_song = songs[0]\n", + "print(\"\\nExample song: \")\n", + "print(example_song)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hKF3EHJlCAj2" + }, + "source": [ + "We can easily convert a song in ABC notation to an audio waveform and play it back. Be patient for this conversion to run, it can take some time." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "11toYzhEEKDz", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 76 + }, + "outputId": "4e357c3a-5930-4202-bf85-5b8d8138a9ee" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {}, + "execution_count": 4 + } + ], + "source": [ + "# Convert the ABC notation to audio file and listen to it\n", + "mdl.lab1.play_song(example_song)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7vH24yyquwKQ" + }, + "source": [ + "One important thing to think about is that this notation of music does not simply contain information on the notes being played, but additionally there is meta information such as the song title, key, and tempo. How does the number of different characters that are present in the text file impact the complexity of the learning problem? This will become important soon, when we generate a numerical representation for the text data." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "id": "IlCgQBRVymwR", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "1da28f96-951d-4b8a-aba4-7ab79fd682f0" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "There are 83 unique characters in the dataset\n", + "['\\n', ' ', '!', '\"', '#', \"'\", '(', ')', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', '<', '=', '>', 'A', 'B', 'C', 'D']\n", + "len(songs)=817\n", + "len(songs[0])=252\n", + "len(songs[5])=234\n" + ] + } + ], + "source": [ + "# Join our list of song strings into a single string containing all songs\n", + "songs_joined = \"\\n\\n\".join(songs)\n", + "\n", + "# Find all unique characters in the joined string\n", + "vocab = sorted(set(songs_joined))\n", + "print(\"There are\", len(vocab), \"unique characters in the dataset\")\n", + "print(vocab[:30])\n", + "print(f'{len(songs)=}')\n", + "print(f'{len(songs[0])=}')\n", + "print(f'{len(songs[5])=}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rNnrKn_lL-IJ" + }, + "source": [ + "## 2.3 Process the dataset for the learning task\n", + "\n", + "Let's take a step back and consider our prediction task. We're trying to train an RNN model to learn patterns in ABC music, and then use this model to generate (i.e., predict) a new piece of music based on this learned information.\n", + "\n", + "Breaking this down, what we're really asking the model is: given a character, or a sequence of characters, what is the most probable next character? We'll train the model to perform this task.\n", + "\n", + "To achieve this, we will input a sequence of characters to the model, and train the model to predict the output, that is, the following character at each time step. RNNs maintain an internal state that depends on previously seen elements, so information about all characters seen up until a given moment will be taken into account in generating the prediction." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LFjSVAlWzf-N" + }, + "source": [ + "### Vectorize the text\n", + "\n", + "Before we begin training our RNN model, we'll need to create a numerical representation of our text-based dataset. To do this, we'll generate two lookup tables: one that maps characters to numbers, and a second that maps numbers back to characters. Recall that we just identified the unique characters present in the text.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "IalZLbvOzf-F" + }, + "outputs": [], + "source": [ + "### Define numerical representation of text ###\n", + "\n", + "# Create a mapping from character to unique index.\n", + "# For example, to get the index of the character \"d\",\n", + "# we can evaluate `char2idx[\"d\"]`.\n", + "char2idx = {u: i for i, u in enumerate(vocab)}\n", + "\n", + "# Create a mapping from indices to characters. This is\n", + "# the inverse of char2idx and allows us to convert back\n", + "# from unique index to the character in our vocabulary.\n", + "idx2char = np.array(vocab)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tZfqhkYCymwX" + }, + "source": [ + "This gives us an integer representation for each character. Observe that the unique characters (i.e., our vocabulary) in the text are mapped as indices from 0 to `len(unique)`. Let's take a peek at this numerical representation of our dataset:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "FYyNlCNXymwY", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "b4f230e1-5bb0-4942-be9f-05a20f335428" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "{\n", + " '\\n': 0,\n", + " ' ' : 1,\n", + " '!' : 2,\n", + " '\"' : 3,\n", + " '#' : 4,\n", + " \"'\" : 5,\n", + " '(' : 6,\n", + " ')' : 7,\n", + " ',' : 8,\n", + " '-' : 9,\n", + " '.' : 10,\n", + " '/' : 11,\n", + " '0' : 12,\n", + " '1' : 13,\n", + " '2' : 14,\n", + " '3' : 15,\n", + " '4' : 16,\n", + " '5' : 17,\n", + " '6' : 18,\n", + " '7' : 19,\n", + " ...\n", + "}\n" + ] + } + ], + "source": [ + "print('{')\n", + "for char, _ in zip(char2idx, range(20)):\n", + " print(' {:4s}: {:3d},'.format(repr(char), char2idx[char]))\n", + "print(' ...\\n}')" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "id": "g-LnKyu4dczc" + }, + "outputs": [], + "source": [ + "### Vectorize the songs string ###\n", + "\n", + "'''TODO: Write a function to convert the all songs string to a vectorized\n", + " (i.e., numeric) representation. Use the appropriate mapping\n", + " above to convert from vocab characters to the corresponding indices.\n", + "\n", + " NOTE: the output of the `vectorize_string` function\n", + " should be a np.array with `N` elements, where `N` is\n", + " the number of characters in the input string\n", + "'''\n", + "def vectorize_string(string):\n", + " '''TODO'''\n", + " return np.array([char2idx[x] for x in string])\n", + "\n", + "vectorized_songs = vectorize_string(songs_joined)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IqxpSuZ1w-ub" + }, + "source": [ + "We can also look at how the first part of the text is mapped to an integer representation:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "l1VKcQHcymwb", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "3e36859d-be4c-45b4-984c-a10e221d7cb4" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "'X:1\\nT:Alex' ---- characters mapped to int ----> [49 22 13 0 45 22 26 67 60 79]\n" + ] + } + ], + "source": [ + "print ('{} ---- characters mapped to int ----> {}'.format(repr(songs_joined[:10]), vectorized_songs[:10]))\n", + "# check that vectorized_songs is a numpy array\n", + "assert isinstance(vectorized_songs, np.ndarray), \"returned result should be a numpy array\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hgsVvVxnymwf" + }, + "source": [ + "### Create training examples and targets\n", + "\n", + "Our next step is to actually divide the text into example sequences that we'll use during training. Each input sequence that we feed into our RNN will contain `seq_length` characters from the text. We'll also need to define a target sequence for each input sequence, which will be used in training the RNN to predict the next character. For each input, the corresponding target will contain the same length of text, except shifted one character to the right.\n", + "\n", + "To do this, we'll break the text into chunks of `seq_length+1`. Suppose `seq_length` is 4 and our text is \"Hello\". Then, our input sequence is \"Hell\" and the target sequence is \"ello\".\n", + "\n", + "The batch method will then let us convert this stream of character indices to sequences of the desired size.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "id": "LF-N8F7BoDRi", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "ee83d681-f8d6-4731-be65-cf98f661b68c" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Batch function works correctly!\n" + ] + } + ], + "source": [ + "### Batch definition to create training examples ###\n", + "\n", + "def get_batch(vectorized_songs, seq_length, batch_size):\n", + " # the length of the vectorized songs string\n", + " n = vectorized_songs.shape[0] - 1\n", + " # randomly choose the starting indices for the examples in the training batch\n", + " idx = np.random.choice(n - seq_length, batch_size)\n", + "\n", + " '''TODO: construct a list of input sequences for the training batch'''\n", + " input_batch = np.stack([vectorized_songs[i:i+seq_length] for i in idx])\n", + " '''TODO: construct a list of output sequences for the training batch'''\n", + " output_batch = np.stack([vectorized_songs[i+1:i+seq_length+1] for i in idx])\n", + "\n", + "\n", + " # Convert the input and output batches to tensors\n", + " x_batch = torch.tensor(input_batch, dtype=torch.long)\n", + " y_batch = torch.tensor(output_batch, dtype=torch.long)\n", + "\n", + " return x_batch, y_batch\n", + "\n", + "# Perform some simple tests to make sure your batch function is working properly!\n", + "test_args = (vectorized_songs, 10, 2)\n", + "x_batch, y_batch = get_batch(*test_args)\n", + "assert x_batch.shape == (2, 10), \"x_batch shape is incorrect\"\n", + "assert y_batch.shape == (2, 10), \"y_batch shape is incorrect\"\n", + "print(\"Batch function works correctly!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_33OHL3b84i0" + }, + "source": [ + "For each of these vectors, each index is processed at a single time step. So, for the input at time step 0, the model receives the index for the first character in the sequence, and tries to predict the index of the next character. At the next timestep, it does the same thing, but the RNN considers the information from the previous step, i.e., its updated state, in addition to the current input.\n", + "\n", + "We can make this concrete by taking a look at how this works over the first several characters in our text:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "id": "0eBu9WZG84i0", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "27c8d3ce-b762-4f83-86b1-1edff0674b7e" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Step 0\n", + " input: 73 (np.str_('r'))\n", + " expected output: 0 (np.str_('\\n'))\n", + "Step 1\n", + " input: 0 (np.str_('\\n'))\n", + " expected output: 59 (np.str_('d'))\n", + "Step 2\n", + " input: 59 (np.str_('d'))\n", + " expected output: 32 (np.str_('G'))\n", + "Step 3\n", + " input: 32 (np.str_('G'))\n", + " expected output: 32 (np.str_('G'))\n", + "Step 4\n", + " input: 32 (np.str_('G'))\n", + " expected output: 14 (np.str_('2'))\n" + ] + } + ], + "source": [ + "x_batch, y_batch = get_batch(vectorized_songs, seq_length=5, batch_size=1)\n", + "\n", + "for i, (input_idx, target_idx) in enumerate(zip(x_batch[0], y_batch[0])):\n", + " print(\"Step {:3d}\".format(i))\n", + " print(\" input: {} ({:s})\".format(input_idx, repr(idx2char[input_idx.item()])))\n", + " print(\" expected output: {} ({:s})\".format(target_idx, repr(idx2char[target_idx.item()])))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "r6oUuElIMgVx" + }, + "source": [ + "## 2.4 The Recurrent Neural Network (RNN) model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m8gPwEjRzf-Z" + }, + "source": [ + "Now we're ready to define and train an RNN model on our ABC music dataset, and then use that trained model to generate a new song. We'll train our RNN using batches of song snippets from our dataset, which we generated in the previous section.\n", + "\n", + "The model is based off the LSTM architecture, where we use a state vector to maintain information about the temporal relationships between consecutive characters. The final output of the LSTM is then fed into a fully connected linear [`nn.Linear`](https://pytorch.org/docs/stable/generated/torch.nn.Linear.html) layer where we'll output a softmax over each character in the vocabulary, and then sample from this distribution to predict the next character.\n", + "\n", + "As we introduced in the first portion of this lab, we'll be using PyTorch's [`nn.Module`](https://pytorch.org/docs/stable/generated/torch.nn.Module.html) to define the model. Three components are used to define the model:\n", + "\n", + "* [`nn.Embedding`](https://pytorch.org/docs/stable/generated/torch.nn.Embedding.html): This is the input layer, consisting of a trainable lookup table that maps the numbers of each character to a vector with `embedding_dim` dimensions.\n", + "* [`nn.LSTM`](https://pytorch.org/docs/stable/generated/torch.nn.LSTM.html): Our LSTM network, with size `hidden_size`.\n", + "* [`nn.Linear`](https://pytorch.org/docs/stable/generated/torch.nn.Linear.html): The output layer, with `vocab_size` outputs.\n", + "\n", + "\"Drawing\"/\n", + "\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rlaOqndqBmJo" + }, + "source": [ + "### Define the RNN model\n", + "\n", + "Let's define our model as an `nn.Module`. Fill in the `TODOs` to define the RNN model.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "id": "8DsWzojvkbc7" + }, + "outputs": [], + "source": [ + "### Defining the RNN Model ###\n", + "\n", + "'''TODO: Add LSTM and Linear layers to define the RNN model using nn.Module'''\n", + "class LSTMModel(nn.Module):\n", + " def __init__(self, vocab_size, embedding_dim, hidden_size):\n", + " super(LSTMModel, self).__init__()\n", + " self.hidden_size = hidden_size\n", + "\n", + " # Define each of the network layers\n", + " # Layer 1: Embedding layer to transform indices into dense vectors\n", + " # of a fixed embedding size\n", + " self.embedding = nn.Embedding(vocab_size, embedding_dim)\n", + "\n", + " '''TODO: Layer 2: LSTM with hidden_size `hidden_size`. note: number of layers defaults to 1.\n", + " Use the nn.LSTM() module from pytorch.'''\n", + " self.lstm = nn.LSTM(embedding_dim, self.hidden_size, batch_first=True) # TODO\n", + "\n", + " '''TODO: Layer 3: Linear (fully-connected) layer that transforms the LSTM output\n", + " # into the vocabulary size.'''\n", + " self.fc = nn.Linear(self.hidden_size, vocab_size) # TODO\n", + "\n", + " def init_hidden(self, batch_size, device):\n", + " # Initialize hidden state and cell state with zeros\n", + " return (torch.zeros(1, batch_size, self.hidden_size).to(device),\n", + " torch.zeros(1, batch_size, self.hidden_size).to(device))\n", + "\n", + " def forward(self, x, state=None, return_state=False):\n", + " x = self.embedding(x)\n", + "\n", + " if state is None:\n", + " state = self.init_hidden(x.size(0), x.device)\n", + " out, state = self.lstm(x, state)\n", + "\n", + " out = self.fc(out)\n", + " return out if not return_state else (out, state)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IbWU4dMJmMvq" + }, + "source": [ + "The time has come! Let's instantiate the model!" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "id": "MtCrdfzEI2N0", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "f5110422-9247-4657-a1e6-4ccc7ba2bb7e" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "LSTMModel(\n", + " (embedding): Embedding(83, 256)\n", + " (lstm): LSTM(256, 1024, batch_first=True)\n", + " (fc): Linear(in_features=1024, out_features=83, bias=True)\n", + ")\n" + ] + } + ], + "source": [ + "# Instantiate the model! Build a simple model with default hyperparameters. You\n", + "# will get the chance to change these later.\n", + "vocab_size = len(vocab)\n", + "embedding_dim = 256\n", + "hidden_size = 1024\n", + "batch_size = 8\n", + "\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "\n", + "model = LSTMModel(vocab_size, embedding_dim, hidden_size).to(device)\n", + "\n", + "# print out a summary of the model\n", + "print(model)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-ubPo0_9Prjb" + }, + "source": [ + "### Test out the RNN model\n", + "\n", + "It's always a good idea to run a few simple checks on our model to see that it behaves as expected. \n", + "\n", + "We can quickly check the layers in the model, the shape of the output of each of the layers, the batch size, and the dimensionality of the output. Note that the model can be run on inputs of any length." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "id": "C-_70kKAPrPU", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "cdc73b33-2bdb-47b6-d1fd-ccb6108cd539" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Input shape: torch.Size([32, 100]) # (batch_size, sequence_length)\n", + "Prediction shape: torch.Size([32, 100, 83]) # (batch_size, sequence_length, vocab_size)\n", + "y.shape=torch.Size([32, 100])\n" + ] + } + ], + "source": [ + "# Test the model with some sample data\n", + "x, y = get_batch(vectorized_songs, seq_length=100, batch_size=32)\n", + "x = x.to(device)\n", + "y = y.to(device)\n", + "\n", + "pred = model(x)\n", + "print(\"Input shape: \", x.shape, \" # (batch_size, sequence_length)\")\n", + "print(\"Prediction shape: \", pred.shape, \"# (batch_size, sequence_length, vocab_size)\")\n", + "print(f'{y.shape=}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mT1HvFVUGpoE" + }, + "source": [ + "### Predictions from the untrained model\n", + "\n", + "Let's take a look at what our untrained model is predicting.\n", + "\n", + "To get actual predictions from the model, we sample from the output distribution, which is defined by a torch.softmax over our character vocabulary. This will give us actual character indices. This means we are using a [categorical distribution](https://en.wikipedia.org/wiki/Categorical_distribution) to sample over the example prediction. This gives a prediction of the next character (specifically its index) at each timestep. [`torch.multinomial`](https://pytorch.org/docs/stable/generated/torch.multinomial.html#torch.multinomial) samples over a categorical distribution to generate predictions.\n", + "\n", + "Note here that we sample from this probability distribution, as opposed to simply taking the `argmax`, which can cause the model to get stuck in a repetitive loop.\n", + "\n", + "Let's try this sampling out for the first example in the batch." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "id": "4V4MfFg0RQJg", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "2a6ab8d4-a66c-4182-b48a-58887af36f94" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "array([39, 44, 77, 1, 45, 54, 42, 10, 17, 19, 17, 1, 68, 75, 82, 50, 18,\n", + " 27, 37, 13, 53, 34, 77, 42, 4, 49, 74, 17, 24, 71, 51, 5, 41, 42,\n", + " 32, 67, 57, 32, 32, 67, 38, 70, 11, 16, 77, 64, 48, 41, 68, 82, 47,\n", + " 16, 52, 72, 75, 15, 4, 42, 36, 3, 10, 46, 6, 42, 24, 68, 70, 67,\n", + " 48, 34, 81, 42, 43, 21, 27, 34, 31, 29, 62, 42, 37, 28, 73, 56, 57,\n", + " 71, 57, 73, 23, 68, 16, 30, 43, 12, 19, 17, 6, 47, 42, 38])" + ] + }, + "metadata": {}, + "execution_count": 34 + } + ], + "source": [ + "sampled_indices = torch.multinomial(torch.softmax(pred[0], dim=-1), num_samples=1)\n", + "sampled_indices = sampled_indices.squeeze(-1).cpu().numpy()\n", + "sampled_indices" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LfLtsP3mUhCG" + }, + "source": [ + "We can now decode these to see the text predicted by the untrained model:" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "id": "xWcFwPwLSo05", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "be22ab99-546b-486d-deac-19fb5c99c42f" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Input: \n", + " ':1/8\\nK:A Mixolydian\\ned|cBcd cBAG|AGEF GABd|cdcB AGEG|ABcd e2ed|!\\ncBcd cBAG|AEAB cdcB|ABAG EDEG|A4 A2'\n", + "\n", + "Next Char Predictions: \n", + " 'NSv T^Q.575 mt|Y6BL1]IvQ#Xs5=pZ\\'PQGlbGGlMo/4viWPm|V4[qt3#QK\".U(Q=molWIzQR9BIFDgQLCrabpbr" + ], + "image/png": "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\n" + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "100%|██████████| 3000/3000 [01:37<00:00, 30.67it/s]\n", + "\u001b[1;38;5;39mCOMET INFO:\u001b[0m Uploading 112 metrics, params and output messages\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "True" + ] + }, + "metadata": {}, + "execution_count": 47 + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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\n" + }, + "metadata": {} + } + ], + "source": [ + "### Define optimizer and training operation ###\n", + "\n", + "'''TODO: instantiate a new LSTMModel model for training using the hyperparameters\n", + " created above.'''\n", + "model = LSTMModel(vocab_size=vocab_size, embedding_dim=params['embedding_dim'],\n", + " hidden_size=params['hidden_size'])\n", + "\n", + "# Move the model to the GPU\n", + "model.to(device)\n", + "\n", + "'''TODO: instantiate an optimizer with its learning rate.\n", + " Checkout the PyTorch website for a list of supported optimizers.\n", + " https://pytorch.org/docs/stable/optim.html\n", + " Try using the Adam optimizer to start.'''\n", + "optimizer = optim.Adam(model.parameters(), lr=params[\"learning_rate\"])\n", + "\n", + "def train_step(x, y):\n", + " # Set the model's mode to train\n", + " model.train()\n", + "\n", + " # Zero gradients for every step\n", + " optimizer.zero_grad()\n", + "\n", + " # Forward pass\n", + " '''TODO: feed the current input into the model and generate predictions'''\n", + " y_hat = model(x)\n", + "\n", + " # Compute the loss\n", + " '''TODO: compute the loss!'''\n", + " loss = compute_loss(y, y_hat)\n", + "\n", + " # Backward pass\n", + " '''TODO: complete the gradient computation and update step.\n", + " Remember that in PyTorch there are two steps to the training loop:\n", + " 1. Backpropagate the loss\n", + " 2. Update the model parameters using the optimizer\n", + " '''\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " return loss\n", + "\n", + "##################\n", + "# Begin training!#\n", + "##################\n", + "\n", + "history = []\n", + "plotter = mdl.util.PeriodicPlotter(sec=2, xlabel='Iterations', ylabel='Loss')\n", + "experiment = create_experiment()\n", + "\n", + "if hasattr(tqdm, '_instances'): tqdm._instances.clear() # clear if it exists\n", + "for iter in tqdm(range(params[\"num_training_iterations\"])):\n", + "\n", + " # Grab a batch and propagate it through the network\n", + " x_batch, y_batch = get_batch(vectorized_songs, params[\"seq_length\"], params[\"batch_size\"])\n", + "\n", + " # Convert numpy arrays to PyTorch tensors\n", + " x_batch = torch.tensor(x_batch, dtype=torch.long).to(device)\n", + " y_batch = torch.tensor(y_batch, dtype=torch.long).to(device)\n", + "\n", + " # Take a train step\n", + " loss = train_step(x_batch, y_batch)\n", + "\n", + " # Log the loss to the Comet interface\n", + " experiment.log_metric(\"loss\", loss.item(), step=iter)\n", + "\n", + " # Update the progress bar and visualize within notebook\n", + " history.append(loss.item())\n", + " plotter.plot(history)\n", + "\n", + " # Save model checkpoint\n", + " if iter % 100 == 0:\n", + " torch.save(model.state_dict(), checkpoint_prefix)\n", + "\n", + "# Save the final trained model\n", + "torch.save(model.state_dict(), checkpoint_prefix)\n", + "experiment.flush()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kKkD5M6eoSiN" + }, + "source": [ + "## 2.6 Generate music using the RNN model\n", + "\n", + "Now, we can use our trained RNN model to generate some music! When generating music, we'll have to feed the model some sort of seed to get it started (because it can't predict anything without something to start with!).\n", + "\n", + "Once we have a generated seed, we can then iteratively predict each successive character (remember, we are using the ABC representation for our music) using our trained RNN. More specifically, recall that our RNN outputs a `softmax` over possible successive characters. For inference, we iteratively sample from these distributions, and then use our samples to encode a generated song in the ABC format.\n", + "\n", + "Then, all we have to do is write it to a file and listen!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DjGz1tDkzf-u" + }, + "source": [ + "### The prediction procedure\n", + "\n", + "Now, we're ready to write the code to generate text in the ABC music format:\n", + "\n", + "* Initialize a \"seed\" start string and the RNN state, and set the number of characters we want to generate.\n", + "\n", + "* Use the start string and the RNN state to obtain the probability distribution over the next predicted character.\n", + "\n", + "* Sample from multinomial distribution to calculate the index of the predicted character. This predicted character is then used as the next input to the model.\n", + "\n", + "* At each time step, the updated RNN state is fed back into the model, so that it now has more context in making the next prediction. After predicting the next character, the updated RNN states are again fed back into the model, which is how it learns sequence dependencies in the data, as it gets more information from the previous predictions.\n", + "\n", + "![LSTM inference](https://raw.githubusercontent.com/MITDeepLearning/introtodeeplearning/2019/lab1/img/lstm_inference.png)\n", + "\n", + "Complete and experiment with this code block (as well as some of the aspects of network definition and training!), and see how the model performs. How do songs generated after training with a small number of epochs compare to those generated after a longer duration of training?" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "id": "WvuwZBX5Ogfd" + }, + "outputs": [], + "source": [ + "### Prediction of a generated song ###\n", + "\n", + "def generate_text(model, start_string, generation_length=1000):\n", + " # Evaluation step (generating ABC text using the learned RNN model)\n", + "\n", + " '''TODO: convert the start string to numbers (vectorize)'''\n", + " input_idx = [char2idx[x] for x in start_string] # TODO\n", + " input_idx = torch.tensor([input_idx], dtype=torch.long).to(device)\n", + "\n", + " # Initialize the hidden state\n", + " state = model.init_hidden(input_idx.size(0), device)\n", + "\n", + " # Empty string to store our results\n", + " text_generated = []\n", + " tqdm._instances.clear()\n", + "\n", + " for i in tqdm(range(generation_length)):\n", + " '''TODO: evaluate the inputs and generate the next character predictions'''\n", + " predictions, hidden_state = model(input_idx, state, return_state=True) # TODO\n", + "\n", + " # Remove the batch dimension\n", + " predictions = predictions.squeeze(0)\n", + "\n", + " '''TODO: use a multinomial distribution to sample over the probabilities'''\n", + " input_idx = torch.multinomial(torch.softmax(predictions, dim=-1), num_samples=1) # TODO\n", + "\n", + " # sampled_indices = torch.multinomial(torch.softmax(pred[0], dim=-1), num_samples=1)\n", + " # sampled_indices = sampled_indices.squeeze(-1).cpu().numpy()\n", + "\n", + " '''TODO: add the predicted character to the generated text!'''\n", + " # Hint: consider what format the prediction is in vs. the output\n", + " text_generated.append(idx2char[input_idx[-1]]) # TODO\n", + "\n", + " return (start_string + ''.join(text_generated))" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "id": "ktovv0RFhrkn", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "50b656fe-f087-4e4f-c4bb-f2af38a029fc" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "100%|██████████| 1000/1000 [00:00<00:00, 1699.34it/s]" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "X:13|GE3d:Rubigfdfef|!\n", + "GBElinora gedB DcB,CEAc|FF|!\n", + "K:T: cdBABAcA2FG orga^cBA2 f2 ba^cdBl\n", + "B BE2e c igc Aee F|AGB|fg|B dfforajid:GBAGE2F G/8\n", + "M:|(3\n", + "Ma|D dBA2|!\n", + "Rubaf dBAFAGF edBdBc|cB/8\n", + "Ma DEDa B|!\n", + "zee\n", + "Ma2G2cAGB|g|bubay e|!\n", + "M/8\n", + "Ma A|G WiafdBA c|!\n", + "fdB DG AB,|]!\n", + "Ma E\n", + "X:dc|2es\n", + "Bcegggfge^cd|dB2GEDDClig2ed2e-18\n", + "f|d|!\n", + "Z:Dor d|!\n", + "dBd|c|BA ChaA|cA|DbG ABE\n", + "Ma dBtef EF|!\n", + "M:M:3|GF eddfdBABA cABG2AGD AG/8\n", + "Z:|AK:2e|FAABA f a|BcBcd|D A2defd|cBA ed|A dBA|dBAd3 EA afgeefeddBAdef|cB,2GBcBA|ABd|DDG:9\n", + "\n", + "M:12edBc|!\n", + "F ecAcdB|DF G3\n", + "Ma G2 DE2|DGB|ceA,C=cBABc|]!\n", + "B GED GBA/8\n", + "\n", + "M:|DGG Ac|g|bafgecBA D2|G2A A2AG GF g|d:1/8\n", + "M:|GB M:|]!\n", + "A|c|G,2 c d>d|dBA|aa er\n", + "M:10\n", + "Mig|G/8\n", + "Bdfgelig2 ororororor\n", + "L:DF d:d|A2:dAGGAG Wig|G|]!\n", + "T:|!\n", + "Ma a2G eA _!\n", + "M:BdB|!\n", + "M: ig2F F cA =cc|cBA|!\n", + "Mag-4 cB|\n", + "dBd|fag2 g|cBA380\n", + "Z:DE|A|cA|cecd a Bgdd|GGE2df|G GD cAc|Ad|!\n", + "dF:d:Djid|!\n", + "DD BA2:C\n", + "Ma|E|cAD d|!\n", + "edd|!\n", + "L:de2GBAF BAG BD:3=cdBA|!\n", + "M:1b2dBA|dBD AcBAGB,dBA2:D Ad|]!\n", + "Z:1/8\n", + "Min\n", + "M: efababa|dfab|ceA2GBAct BcA=cdBDG2dfgf|dB E dBADGBABA|fdB AB cAGE2 BGG d|d\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "\n" + ] + } + ], + "source": [ + "'''TODO: Use the model and the function defined above to generate ABC format text of length 1000!\n", + " As you may notice, ABC files start with \"X\" - this may be a good start string.'''\n", + "generated_text = generate_text(model,'X') # TODO\n", + "print(generated_text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AM2Uma_-yVIq" + }, + "source": [ + "### Play back the generated music!\n", + "\n", + "We can now call a function to convert the ABC format text to an audio file, and then play that back to check out our generated music! Try training longer if the resulting song is not long enough, or re-generating the song!\n", + "\n", + "We will save the song to Comet -- you will be able to find your songs under the `Audio` and `Assets & Artifacts` pages in your Comet interface for the project. Note the [`log_asset()`](https://www.comet.com/docs/v2/api-and-sdk/python-sdk/reference/Experiment/#experimentlog_asset) documentation, where you will see how to specify file names and other parameters for saving your assets." + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "id": "LrOtG64bfLto", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "dcd8c7f2-c616-4f38-acc0-18b374b8161d" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Found 2 songs in text\n", + "['X:13|GE3d:Rubigfdfef|!\\nGBElinora gedB DcB,CEAc|FF|!\\nK:T: cdBABAcA2FG orga^cBA2 f2 ba^cdBl\\nB BE2e c igc Aee F|AGB|fg|B dfforajid:GBAGE2F G/8\\nM:|(3\\nMa|D dBA2|!\\nRubaf dBAFAGF edBdBc|cB/8\\nMa DEDa B|!\\nzee\\nMa2G2cAGB|g|bubay e|!\\nM/8\\nMa A|G WiafdBA c|!\\nfdB DG AB,|]!\\nMa E\\nX:dc|2es\\nBcegggfge^cd|dB2GEDDClig2ed2e-18\\nf|d|!\\nZ:Dor d|!\\ndBd|c|BA ChaA|cA|DbG ABE\\nMa dBtef EF|!\\nM:M:3|GF eddfdBABA cABG2AGD AG/8\\nZ:|AK:2e|FAABA f a|BcBcd|D A2defd|cBA ed|A dBA|dBAd3 EA afgeefeddBAdef|cB,2GBcBA|ABd|DDG:9', 'M:12edBc|!\\nF ecAcdB|DF G3\\nMa G2 DE2|DGB|ceA,C=cBABc|]!\\nB GED GBA/8']\n", + "None\n", + "None\n" + ] + } + ], + "source": [ + "### Play back generated songs ###\n", + "\n", + "generated_songs = mdl.lab1.extract_song_snippet(generated_text)\n", + "print(generated_songs)\n", + "\n", + "for i, song in enumerate(generated_songs):\n", + " # Synthesize the waveform from a song\n", + " waveform = mdl.lab1.play_song(song)\n", + " print(waveform)\n", + "\n", + " # If its a valid song (correct syntax), lets play it!\n", + " if waveform:\n", + " print(\"Generated song\", i)\n", + " ipythondisplay.display(waveform)\n", + "\n", + " numeric_data = np.frombuffer(waveform.data, dtype=np.int16)\n", + " wav_file_path = f\"output_{i}.wav\"\n", + " write(wav_file_path, 88200, numeric_data)\n", + "\n", + " # save your song to the Comet interface -- you can access it there\n", + " experiment.log_asset(wav_file_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4353qSV76gnJ" + }, + "outputs": [], + "source": [ + "# when done, end the comet experiment\n", + "experiment.end()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HgVvcrYmSKGG" + }, + "source": [ + "## 2.7 Experiment and **get awarded for the best songs**!\n", + "\n", + "Congrats on making your first sequence model in TensorFlow! It's a pretty big accomplishment, and hopefully you have some sweet tunes to show for it.\n", + "\n", + "Consider how you may improve your model and what seems to be most important in terms of performance. Here are some ideas to get you started:\n", + "\n", + "* How does the number of training epochs affect the performance?\n", + "* What if you alter or augment the dataset?\n", + "* Does the choice of start string significantly affect the result?\n", + "\n", + "Try to optimize your model and submit your best song! **Participants will be eligible for prizes during the January 2025 offering. To enter the competition, you must upload the following to [this submission link](https://www.dropbox.com/request/U8nND6enGjirujVZKX1n):**\n", + "\n", + "* a recording of your song;\n", + "* iPython notebook with the code you used to generate the song;\n", + "* a description and/or diagram of the architecture and hyperparameters you used -- if there are any additional or interesting modifications you made to the template code, please include these in your description.\n", + "\n", + "**Name your file in the following format: ``[FirstName]_[LastName]_RNNMusic``, followed by the file format (.zip, .mp4, .ipynb, .pdf, etc). ZIP files of all three components are preferred over individual files. If you submit individual files, you must name the individual files according to the above nomenclature.**\n", + "\n", + "You can also tweet us at [@MITDeepLearning](https://twitter.com/MITDeepLearning) a copy of the song (but this will not enter you into the competition)! See this example song generated by a previous student (credit Ana Heart): song from May 20, 2020.\n", + "\n", + "\n", + "Have fun and happy listening!\n", + "\n", + "![Let's Dance!](http://33.media.tumblr.com/3d223954ad0a77f4e98a7b87136aa395/tumblr_nlct5lFVbF1qhu7oio1_500.gif)\n" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "collapsed_sections": [ + "uoJsVjtCMunI" + ], + "name": "PT_Part2_Music_Generation.ipynb", + "provenance": [], + "include_colab_link": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/misc/algos/palindrome.spec.js b/misc/algos/palindrome.spec.js new file mode 100644 index 0000000000..037ebc0f42 --- /dev/null +++ b/misc/algos/palindrome.spec.js @@ -0,0 +1,114 @@ +// Given a string, find the palindrome that can be made by inserting the fewest +// number of characters as possible anywhere in the word. If there is more than +// one palindrome of minimum length that can be made, return the lexicographically +// earliest one (the first one alphabetically). + +// For example, given the string "race", you should return "ecarace", since we +// can add three letters to it (which is the smallest amount to make a +// palindrome). There are seven other palindromes that can be made from "race" +// by adding three letters, but "ecarace" comes first alphabetically. + +// As another example, given the string "google", you should return "elgoogle". + +const assert = chai.assert, + pr = console.log; + +describe('palindrome', () => { + it('isPal', () => { + assert.ok(isPal('a')); + assert.notOk(isPal('ab')); + assert.notOk(isPal('abc')); + + assert.ok(isPal('aa')); + assert.ok(isPal('aba')); + assert.ok(isPal('abba')); + assert.ok(isPal('abcba')); + }); + + it('children', () => { + const set = (xs) => new Set(xs); + assert.deepEqual(set(['aba', 'bab']), set(['aba', 'bab'])); + assert.deepEqual(set(['aba', 'bab']), set(children('ab'))); + assert.deepEqual(set(['cabc', 'abca']), set(children('abc'))); + assert.deepEqual( + set(['acbcd', 'dabcd', 'abcda', 'abcbd']), + set(children('abcd')), + ); + // pr('abca', children('abca')); + }); + + it('isBetter', () => { + assert.ok('abc' > 'aabc'); + assert.ok(isBetter(null, 'a')); + assert.ok(isBetter('aa', 'a')); + assert.ok(isBetter('ab', 'aa')); + assert.notOk(isBetter('ab', 'aab')); + assert.notOk(isBetter('ab', 'ad')); + }); + + it('makePalindrome', () => { + assert.equal('a', makePalindrome('a')); + assert.equal('aa', makePalindrome('aa')); + assert.equal('aba', makePalindrome('ab')); + assert.notEqual('bab', makePalindrome('ab')); + assert.equal('abcba', makePalindrome('abc')); + assert.equal('ecarace', makePalindrome('race')); + assert.equal('elgoogle', makePalindrome('google')); + }); +}); + +// str -> str +// bfs +function makePalindrome(s) { + const q = [s], + visited = new Set([s]); + let res = null; + + while (q.length > 0) { + const node = q.shift(); + if (isPal(node) && isBetter(res, node)) { + res = node; + // pr('res', res); + continue; + } + for (const ch of children(node)) { + if (!visited.has(ch) && isBetter(res, ch)) { + q.push(ch); + visited.add(ch); + } + } + } + return res; +} + +// str, str -> bool +function isBetter(a, b) { + if (a == null) return true; + return a && a.length >= b.length && a > b; +} + +// str -> bool +function isPal(s) { + const m = Math.floor(s.length / 2); + if (m == 0) return true; + for (let i = 0; i < m; i++) { + if (s[i] !== s[s.length - i - 1]) return false; + } + return true; +} + +// str -> [str] +function children(s) { + const n = s.length, + m = Math.floor(n / 2), + res = []; + assert(m > 0); + // pr('s', s, 'n', n, 'm', m); + for (let i = 0; i < m; i++) { + const s1 = s.slice(0, n - i) + s[i] + s.slice(n - i), + s2 = s.slice(0, i) + s[n - i - 1] + s.slice(i); + // pr(s1, s2); + res.push(s1, s2); + } + return res; +} diff --git a/misc/algos/tests.html b/misc/algos/tests.html new file mode 100644 index 0000000000..86218f63b8 --- /dev/null +++ b/misc/algos/tests.html @@ -0,0 +1,32 @@ + + + + + Mocha Tests +  + + + + +
+ + + + + + + + + + + + diff --git a/misc/data.json b/misc/data.json new file mode 100644 index 0000000000..7e9c00f6cd --- /dev/null +++ b/misc/data.json @@ -0,0 +1 @@ +{ "ar": [1, 2, 3], "text": "hi there!" } diff --git a/misc/fetch.html b/misc/fetch.html new file mode 100644 index 0000000000..b46a7055cc --- /dev/null +++ b/misc/fetch.html @@ -0,0 +1,22 @@ + + misc + + +
+

content by fetch

+
+
+ +
+

content by embed

+ +
+ + not yet + + + diff --git a/misc/fetch.js b/misc/fetch.js new file mode 100644 index 0000000000..722c146e83 --- /dev/null +++ b/misc/fetch.js @@ -0,0 +1,79 @@ +const pr = console.log; + +// let url = 'https://ipv4-c062-lax009-ix.1.oca.nflxvideo.net/speedtest/range/0-26214400?c=us&n=26421&v=109&e=1662347532&t=Z8Ooso0dW0HLf3O1CYaDqCRxpDgLIWFmu3yPJQ'; + +// let url = +// ' https://speedtest.oacys.com.prod.hosts.ooklaserver.net:8080/download?nocache=8a09403e-b991-4716-a234-eb10ccfea7ae&size=100000&guid=a78f57b0-2d31-11ed-be0d-85d48b1baa5e'; + +let rand_str = '&rnd=' + String(Math.random()).slice(2); +url = + 'https://images.unsplash.com/photo-1514065549995-7706f6017a27?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=349&q=80 '; + +const MaxIter = 1; +let i = 0, + intervalId = null; + +function download(url) { + if (i >= MaxIter) { + clearInterval(intervalId); + return; + } + + i++; + const t0 = new Date(); + + fetch(url, { mode: 'no-cors' }) + .then((response) => response.blob()) + // .then((blob) => streamBlob0(blob, { t0: t0, i: i })); + .then((blob) => downloadImage(blob, { t0: t0, i: i })); +} + +const img = document.querySelector('img'); +function downloadImage(url, options) { + fetch(url, options) + .then((response) => response.blob()) + .then((blob) => window.URL.createObjectURL(blob)) + .then((url) => (img.src = url)) + .catch(console.error); +} + +// let tortoise = './tortoise.png'; +let tortoise = + 'https://mdn.github.io/dom-examples/streams/grayscale-png/tortoise.png'; +downloadImage(url + rand_str); + +function streamBlob0(blob, params) { + let reader = blob.stream().getReader(), + size = 0; + reader.read().then(function processData({ done, value }) { + if (done) { + pr(params.i, 'stream completed'); + const t1 = new Date(); + pr({ t0: params.t0, dt: t1 - params.t0, size: size }); + return; + } + pr('not done yet'); + size += value.length; + return reader.read().then(processData); + }); +} + +// intervalId = setInterval(() => download(url), 1000); + +// fetch or embed text ==================== +let S = null; +function getJson(href, root) { + fetch(href) + .then((response) => response.text()) + .then((text) => { + root.textContent = text; + pr(text); + // const obj = eval(text); + S = JSON.parse(text); + pr(S); + }); +} +getJson('./data.json', document.querySelector('#text')); + +let el = document.querySelector('#embed'); +pr('try to get from embed: ', document.querySelector('#embed')); diff --git a/misc/js0.js b/misc/js0.js new file mode 100644 index 0000000000..1e61c763f6 --- /dev/null +++ b/misc/js0.js @@ -0,0 +1,69 @@ +const pr = console.log; + +const onRightA = (data) => + new Promise((resolve, reject) => { + if (data.a == 1) setTimeout(() => resolve(data), 100); + else reject(`expect data.a == 1 but data.a == ${data.a}`); + }); + +onRightA({ a: 1 }).then((dat) => pr('ok', dat)), (s) => pr('NOT OK', s); + +class Observable { + constructor(obj) { + this.obj = obj; + this.cbs = []; + } + + update(fn) { + const res = fn(this.obj); + for (const cb of this.cbs) { + cb(this.obj); + } + return res; + } + + observe(cb) { + this.cbs.push(cb); + } + + unobserve(cb) { + this.cbs = this.cbs.filter((x) => x != cb); + } +} + +function Cnt(cnt) { + function f(x) { + cnt++; + return x + cnt; + } + f.cnt = () => cnt; + return f; +} + +class EventRegistry { + constructor() { + this.map = new Map(); + } + + fireEvent(event) { + for (const cb of this.map.get(event.name) || []) { + cb(event); + } + } + + listen(eventName, cb) { + const cbs = this.map.get(eventName) || []; + cbs.push(cb); + this.map.set(eventName, cbs); + } + unListen(eventName, cb) { + const cbs = this.map.get(eventName) || []; + this.map.set( + eventName, + cbs.filter((x) => x != cb), + ); + } +} + +// let r = fetch('../data/cities.json'); +// pr(r); diff --git a/misc/js0.spec.js b/misc/js0.spec.js new file mode 100644 index 0000000000..d76c528d52 --- /dev/null +++ b/misc/js0.spec.js @@ -0,0 +1,73 @@ +const assert = chai.assert; + +describe('test Promise', () => { + it('check deepEqual and equal', () => { + assert.notEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); + assert.deepEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); + assert.notDeepEqual({ a: 1, b: 2, c: 4 }, { a: 1, b: 2, c: 0 }); + }); + + it('with done() o.a == 1 is ok', (done) => { + onRightA({ a: 1 }).then((o) => done()); + }); + + it('w/o done() - return Promise o.a == 1 is ok', () => { + return onRightA({ a: 1 }); + }); +}); + +describe('Observable', () => { + it('observe and unobserve', () => { + const o = new Observable({ a: 1 }); + pr('o', o); + const state = { a: 2 }, + add1 = (obj) => (state.a = obj.a + 1), + mult2 = (obj) => (state.b = obj.a * 2); + + o.observe(add1); + o.observe(mult2); + assert.deepEqual({ a: 1 }, o.obj); + assert.deepEqual({ a: 2 }, state); + + o.update((obj) => (obj.a = 3)); + assert.deepEqual({ a: 3 }, o.obj); + assert.deepEqual({ a: 4, b: 6 }, state); + + o.unobserve(add1); + o.update((obj) => (obj.a = 4)); + assert.deepEqual({ a: 4 }, o.obj); + assert.deepEqual({ a: 4, b: 8 }, state); + }); +}); + +describe('Func closures fun', () => { + it('creates fn w/ cnt', () => { + const a = Cnt(0), + b = Cnt(10); + assert.equal(a.cnt(), 0); + assert.equal(b.cnt(), 10); + + assert.equal(a(1), 2); + assert.equal(a.cnt(), 1); + assert.equal(b(2), 13); + assert.equal(b.cnt(), 11); + }); +}); + +describe('EventRegistry', () => { + it('listen, fires and unListen', () => { + const reg = new EventRegistry(), + st1 = { a: 1, b: 2 }, + st2 = {}; + + reg.listen('On', (event) => { + st1.a++; + st1.b += event.param; + st2.a = event.name; + }); + reg.fireEvent({ name: 'On', param: 2 }); + + assert.deepEqual({ a: 2, b: 4 }, st1); + assert.deepEqual({ a: 'On' }, st2); + }); +}); diff --git a/misc/mocha b/misc/mocha new file mode 120000 index 0000000000..d1a9ba35a0 --- /dev/null +++ b/misc/mocha @@ -0,0 +1 @@ +../../mocha/ \ No newline at end of file diff --git a/misc/obj-to-html/index.js b/misc/obj-to-html/index.js new file mode 100644 index 0000000000..97541acb77 --- /dev/null +++ b/misc/obj-to-html/index.js @@ -0,0 +1,93 @@ +const Element = (tag) => document.createElement(tag), + pr = console.log; + +const span = (txt) => { + const el = Element('span'); + el.append(txt); + return el; +}; + +// obj -> DOM Element +export function toHtml(obj) { + if (typeof obj === 'number' || typeof obj === 'string') return span(obj); + if (Array.isArray(obj)) return fromArray(obj); + return fromObject(obj); +} + +function fromArray(obj) { + const el = Element('div'); + el.classList.add('array'); + const els = obj.map((x) => { + const div = Element('div'), + el = toHtml(x); + div.append(el, ', '); + return div; + }); + el.append(...els); + return el; +} + +function fromObject(obj) { + const el = Element('div'), + ul = Element('ul'); + el.append(ul); + el.classList.add('obj'); + + const lis = Object.entries(obj).map(([k, v]) => { + const li = Element('li'), + kdiv = Element('div'), + vdiv = Element('div'), + colon = span(':'); + colon.classList.add('colon'); + + kdiv.append(toHtml(k), colon); + vdiv.append(toHtml(v)); + li.append(kdiv, vdiv); + ul.append(li); + return li; + }); + ul.append(...lis); + return el; +} + +// str -> obj +export const count = (input) => { + const res = {}; + for (const k of input.split('')) { + if (res[k]) { + res[k]++; + } else { + res[k] = 1; + } + } + return res; +}; + +// main ==================== +export function main() { + const root = document.querySelector('#result'), + input = document.querySelector('#input'); + input.addEventListener('keyup', (e) => { + const el = toHtml(count(input.value)); + pr(el); + root.replaceChildren(el); + }); +} +// main end ==================== + +// tests ==================== +export function runInTest() { + const d1 = document.querySelector('#_1'), + o1 = 100, + el1 = toHtml(o1); + d1.replaceChildren(el1); + + const d2 = document.querySelector('#_2'), + o3 = { c: [10, 20, 30, 40], d: 'some text ...' }, + o2 = { a: 1, b: 'foo', ar: [1, 2, 3], o3: o3 }, + el2 = toHtml(o2); + d2.replaceChildren(el2); +} + +// runInTest(); +// tests end ==================== diff --git a/misc/obj-to-html/index.spec.js b/misc/obj-to-html/index.spec.js new file mode 100644 index 0000000000..b36cb6ba86 --- /dev/null +++ b/misc/obj-to-html/index.spec.js @@ -0,0 +1,27 @@ +const assert = chai.assert; +import { toHtml, runInTest, count } from './index.js'; + +const pr = console.log; + +describe('toHtml', () => { + it('number, string', () => { + let obj = 2; + let res = toHtml(obj); + assert.equal('2', res.textContent); + + obj = 'xxx'; + res = toHtml(obj); + assert.equal('xxx', res.textContent); + + obj = { a: 1, b: 'str' }; + res = toHtml(obj); + pr('res', res); + }); + + it('count should count', () => { + assert.deepEqual({ a: 1, b: 2 }, count('bab')); + assert.deepEqual({ a: 3, b: 2 }, count('abaab')); + }); +}); + +runInTest(); diff --git a/misc/obj-to-html/main.css b/misc/obj-to-html/main.css new file mode 100644 index 0000000000..b3d4b7de5d --- /dev/null +++ b/misc/obj-to-html/main.css @@ -0,0 +1,18 @@ +div.mycontent { + margin: 20px; +} + +.obj ul { + padding: 2px; +} + +.obj li { + display: flex; +} + +.array { + display: flex; +} +.colon { + margin: 0 10px; +} diff --git a/misc/obj-to-html/main.html b/misc/obj-to-html/main.html new file mode 100644 index 0000000000..5fd8143f01 --- /dev/null +++ b/misc/obj-to-html/main.html @@ -0,0 +1,16 @@ + + misc + + +

letter count

+
+ +
not ready
+
+ + + + diff --git a/misc/obj-to-html/tests.html b/misc/obj-to-html/tests.html new file mode 100644 index 0000000000..7b0e35db42 --- /dev/null +++ b/misc/obj-to-html/tests.html @@ -0,0 +1,35 @@ + + + + + Mocha Tests + + + + + + +
+ + + + + + + + + + + + +
+
_1
+
_2
+
+ + diff --git a/misc/probability/lib/norm.mjs b/misc/probability/lib/norm.mjs new file mode 100644 index 0000000000..ffbc47b59a --- /dev/null +++ b/misc/probability/lib/norm.mjs @@ -0,0 +1,194 @@ +export const pr = console.log; + +export function normalcdf(X) { + //HASTINGS. MAX ERROR = .000001 + var T = 1 / (1 + 0.2316419 * Math.abs(X)); + var D = 0.3989423 * Math.exp((-X * X) / 2); + var Prob = + D * + T * + (0.3193815 + + T * (-0.3565638 + T * (1.781478 + T * (-1.821256 + T * 1.330274)))); + if (X > 0) { + Prob = 1 - Prob; + } + return Prob; +} + +/// Original C++ implementation found at http://www.wilmott.com/messageview.cfm?catid=10&threadid=38771 +/// C# implementation found at http://weblogs.asp.net/esanchez/archive/2010/07/29/a-quick-and-dirty-implementation-of-excel-norminv-function-in-c.aspx +/* + * Compute the quantile function for the normal distribution. + * + * For small to moderate probabilities, algorithm referenced + * below is used to obtain an initial approximation which is + * polished with a final Newton step. + * + * For very large arguments, an algorithm of Wichura is used. + * + * REFERENCE + * + * Beasley, J. D. and S. G. Springer (1977). + * Algorithm AS 111: The percentage points of the normal distribution, + * Applied Statistics, 26, 118-121. + * + * Wichura, M.J. (1988). + * Algorithm AS 241: The Percentage Points of the Normal Distribution. + * Applied Statistics, 37, 477-484. + */ + +export function normsInv(p, mu, sigma) { + if (p < 0 || p > 1) { + throw 'The probality p must be bigger than 0 and smaller than 1'; + } + if (sigma < 0) { + throw 'The standard deviation sigma must be positive'; + } + + if (p == 0) { + return -Infinity; + } + if (p == 1) { + return Infinity; + } + if (sigma == 0) { + return mu; + } + + var q, r, val; + + q = p - 0.5; + + /*-- use AS 241 --- */ + /* double ppnd16_(double *p, long *ifault)*/ + /* ALGORITHM AS241 APPL. STATIST. (1988) VOL. 37, NO. 3 + + Produces the normal deviate Z corresponding to a given lower + tail area of P; Z is accurate to about 1 part in 10**16. + */ + if (Math.abs(q) <= 0.425) { + /* 0.075 <= p <= 0.925 */ + r = 0.180625 - q * q; + val = + (q * + (((((((r * 2509.0809287301226727 + 33430.575583588128105) * r + + 67265.770927008700853) * + r + + 45921.953931549871457) * + r + + 13731.693765509461125) * + r + + 1971.5909503065514427) * + r + + 133.14166789178437745) * + r + + 3.387132872796366608)) / + (((((((r * 5226.495278852854561 + 28729.085735721942674) * r + + 39307.89580009271061) * + r + + 21213.794301586595867) * + r + + 5394.1960214247511077) * + r + + 687.1870074920579083) * + r + + 42.313330701600911252) * + r + + 1); + } else { + /* closer than 0.075 from {0,1} boundary */ + + /* r = min(p, 1-p) < 0.075 */ + if (q > 0) r = 1 - p; + else r = p; + + r = Math.sqrt(-Math.log(r)); + /* r = sqrt(-log(r)) <==> min(p, 1-p) = exp( - r^2 ) */ + + if (r <= 5) { + /* <==> min(p,1-p) >= exp(-25) ~= 1.3888e-11 */ + r += -1.6; + val = + (((((((r * 7.7454501427834140764e-4 + 0.0227238449892691845833) * r + + 0.24178072517745061177) * + r + + 1.27045825245236838258) * + r + + 3.64784832476320460504) * + r + + 5.7694972214606914055) * + r + + 4.6303378461565452959) * + r + + 1.42343711074968357734) / + (((((((r * 1.05075007164441684324e-9 + 5.475938084995344946e-4) * r + + 0.0151986665636164571966) * + r + + 0.14810397642748007459) * + r + + 0.68976733498510000455) * + r + + 1.6763848301838038494) * + r + + 2.05319162663775882187) * + r + + 1); + } else { + /* very close to 0 or 1 */ + r += -5; + val = + (((((((r * 2.01033439929228813265e-7 + 2.71155556874348757815e-5) * r + + 0.0012426609473880784386) * + r + + 0.026532189526576123093) * + r + + 0.29656057182850489123) * + r + + 1.7848265399172913358) * + r + + 5.4637849111641143699) * + r + + 6.6579046435011037772) / + (((((((r * 2.04426310338993978564e-15 + 1.4215117583164458887e-7) * r + + 1.8463183175100546818e-5) * + r + + 7.868691311456132591e-4) * + r + + 0.0148753612908506148525) * + r + + 0.13692988092273580531) * + r + + 0.59983220655588793769) * + r + + 1); + } + + if (q < 0.0) { + val = -val; + } + } + + return mu + sigma * val; +} + +// inverse of standard normal cdf +export const phi = (p) => normsInv(p, 0, 1); + +function sat() { + const n = 40, + p = 1 / 6, + mu = -5 / 6, + varYi = 25 ** 2 * p * (1 - p); + (varY = n * 25 ** 2 * p * (1 - p)), (stdY = varY ** 0.5); + let sn = 400, + xn = sn / n, + z = ((xn - mu) * n ** 0.5) / varYi ** 0.5; + let res = 1 - normalcdf(z); + pr(`mu: ${mu}`); + pr(`xn: ${xn}, ${xn - mu}`); + pr(`stdY: ${stdY}`); + pr(`z: ${z} prob of ${sn}: ${res}`); + pr(`manual z: ${(40 ** 0.5 * (400 / 40 + 5 / 6)) / varYi ** 0.5}`); + + return res; +} diff --git a/misc/probability/lib/test_t.py b/misc/probability/lib/test_t.py new file mode 100644 index 0000000000..4279f4635b --- /dev/null +++ b/misc/probability/lib/test_t.py @@ -0,0 +1,6 @@ +from scipy import stats + +def test0(): + print(f'norm cdf 0 {stats.norm.cdf(0)}') + print(f'norm cdf 0.999 {stats.norm.cdf(0.999)}') + diff --git a/misc/probability/node_modules b/misc/probability/node_modules new file mode 120000 index 0000000000..4fcb0b8205 --- /dev/null +++ b/misc/probability/node_modules @@ -0,0 +1 @@ +../../rxjs/node_modules/ \ No newline at end of file diff --git a/misc/probability/probab.spec.mjs b/misc/probability/probab.spec.mjs new file mode 100644 index 0000000000..3dd8d240fa --- /dev/null +++ b/misc/probability/probab.spec.mjs @@ -0,0 +1,112 @@ +import { assert } from 'chai'; +import { pr, normalcdf, normsInv, phi } from './lib/norm.mjs'; + +function* dice(sides) { + let i = 1; + for (; ; i++) { + if (i > sides) i = 1; + yield i; + } +} + +xdescribe('dice', () => { + it('cycle', () => { + const d = dice(2), + acc = []; + for (let i = 0; i < 4; i++) { + acc.push(d.next().value); + } + assert.deepEqual([1, 2, 1, 2], acc); + }); +}); + +function primes(n) { + const res = [2]; + for (let i = 3; i <= n; i += 2) { + if (res.every((x) => i % x != 0)) res.push(i); + } + return res; +} + +function* primesGen() { + yield 2; + const primes = new Set([2]); + for (let i = 3; ; i += 2) { + let isPrime = true; + for (const x of primes) { + if (i % x === 0) { + isPrime = false; + break; + } + } + if (isPrime) { + primes.add(i); + yield i; + } + } +} + +function* twoDice(sides) { + for (let i = 1; i <= sides; i++) { + for (let j = 1; j <= sides; j++) { + yield [i, j]; + } + } +} + +function experiment() { + const prs = primes(36), + succ = []; + for (const [a, b] of twoDice(6)) { + if (prs.includes(a + b)) { + succ.push([a, b]); + pr([a, b]); + } + } + let exp = succ.length / 36; + pr(`succ.length: ${succ.length}`); + pr(`exp $1 is ${succ.length / 36}`); + pr(`exp $3 is ${(1 / 6) * 3}`); +} + +// experiment(); + +xdescribe('primes', () => { + it('primes', () => { + let res = primes(10); + assert.deepEqual([2, 3, 5, 7], res); + assert.deepEqual([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31], primes(32)); + }); + + it('primesGen', () => { + let got = [], + pGen = primesGen(); + for (let i = 0; i < 12; i++) { + got.push(pGen.next().value); + } + let exp = primes(38); + // pr(got); + assert.deepEqual(exp, got); + }); +}); + +describe('normal dist', () => { + it('cdf and phi', () => { + let x = 0, + cdf = normalcdf(x), + x_p_phi = phi(normalcdf(x)); + assert.closeTo(0.5, cdf, 0.0001); + assert.closeTo(x, x_p_phi, 0.0001); + }); +}); + +function calc() { + let [a, b] = [2, 8.5], + f = 1 / (b - a), + mu = (b + a) / 2, + vr = (b - a) ** 2 / 12, + p4 = (4 - a) * f, + plog = (Math.E - a) * f; + pr('calc:', f, mu, vr, p4, plog); +} +calc(); diff --git a/misc/sexp/index.js b/misc/sexp/index.js new file mode 100644 index 0000000000..467d8a8af0 --- /dev/null +++ b/misc/sexp/index.js @@ -0,0 +1,121 @@ +// utility +const Element = (tag) => document.createElement(tag), + pr = console.log; +// ==================== + +// types + +// Atom = {Symb, Num, Str} +// Sexp = Atom || List[Sexp] + +class Sexp { + constructor(type, val) { + // type = Symb, Num, Str, List + this.type = type; + this.val = val; + } +} + +// Sexp types +const Symb = 'Symb', + Num = 'Num', + Str = 'Str', + List = 'List'; + +// ==================== + +// generator[str] -> [token] +export function* tokenizer(s) { + let i = 0, + token = ''; + + while (i < s.length) { + for (; i < s.length && s[i].match(/\s/); i++) {} + if (i >= s.length) return; + + const j = i; + if (s[i] === '(' || s[i] === ')') { + i++; + yield s.slice(j, i); + } else if (s[i] === '"') { + i = nextStrToken(s, i); + yield s.slice(j, i); + } else { + i = nextLiteralToken(s, i); + yield s.slice(j, i); + } + } +} + +// nextToken: str, int -> int +function nextStrToken(s, i) { + const j = i; + i++; + for (; i < s.length; i++) { + if (s[i] === '"') return i + 1; + } + return i; +} + +// nextToken: str, int -> int +function nextLiteralToken(s, i) { + const j = i; + i++; + for (; i < s.length && /\S/.test(s[i]) && /[^()"]/.test(s[i]); i++) {} + return i; +} + +// str -> [token] +export function tokenize(s) { + const res = []; + for (const token of tokenizer(s)) { + res.push(token); + } + return res; +} + +// nextToken: str, int -> [str, int] +// function nextToken(s, i) { +// if (i >= s.length) return ['', i]; +// // skip 'spaces' +// for (; i < s.length && s[i].match(/\s/); i++) {} + +// const j = i; + +// if (s[i] === '(' || s[i] === ')') return [s.slice(i, i + 1), i + 1]; + +// if (s[i] === '"') return nextStrToken(s, i); + +// // reach literal terminator +// for (; i < s.length && /\S/.test(s[i]) && /[^()"]/.test(s[i]); i++) {} + +// return [s.slice(j, i), i]; +// } + +// parse: str -> Sexp +function parse(s) { + const g = tokenizer(s); + // generator -> Sexp + function loop(acc) { + let { value, done } = g.next(); + for (; !done; { value, done } = g.next()) { + pr('value', value, 'done', done); + + if (value === '(') acc.push(new Sexp(List, loop([]))); + else if (value === ')') return acc; + else if (str(value)) acc.push(new Sexp(Str, value)); + else if (num(value)) acc.push(new Sexp(Num, Number(value))); + else acc.push(new Sexp(Symb, value)); + } + + return acc; + } + const str = (value) => value.startsWith('"'); + const num = (value) => Number(value); + + return loop([]); +} + +// ==================== + +export { Sexp, Symb, Num, Str, List, parse }; diff --git a/misc/sexp/index.spec.js b/misc/sexp/index.spec.js new file mode 100644 index 0000000000..7b350e841d --- /dev/null +++ b/misc/sexp/index.spec.js @@ -0,0 +1,82 @@ +const assert = chai.assert; +import { + Sexp, + Symb, + Num, + Str, + List, + tokenizer, + tokenize, + parse, +} from './index.js'; + +const pr = console.log; + +describe('tokenizing', () => { + it('Symbol eq', () => { + assert.equal(Symb, Symb); + assert.notEqual(Symb, Str); + }); + it('Sexp eq', () => { + assert.deepEqual(new Sexp(Symb, 'x'), new Sexp(Symb, 'x')); + assert.notDeepEqual(new Sexp(Str, 'x'), new Sexp(Symb, 'x')); + }); + + it('Regex', () => { + assert.ok(/["()]/.test('(')); + assert.ok(/["()]/.test(')')); + assert.ok(/["()]/.test('"')); + assert.ok(/[^"()]/.test('x')); + }); + + it('tokenize', () => { + assert.deepEqual([], tokenize('')); + assert.deepEqual(['(', 'x', ')'], tokenize('(x)')); + assert.deepEqual(['(', ')'], tokenize('()')); + assert.deepEqual(['x'], tokenize('x')); + assert.deepEqual(['xyz'], tokenize(' xyz ')); + assert.deepEqual(['xyz', 'z'], tokenize(' xyz z')); + assert.deepEqual(['xyz', '(', 'xx', ')', '('], tokenize('xyz (xx)(')); + + assert.deepEqual(['"xyz"'], tokenize(' "xyz" ')); + assert.deepEqual(['"x yz"'], tokenize(' "x yz" ')); + assert.deepEqual(['"x (yz)"'], tokenize(' "x (yz)" ')); + assert.deepEqual(['a', '"x (yz)"'], tokenize(' a "x (yz)" ')); + }); + + it('parse', () => { + assert.deepEqual([new Sexp(Symb, 'x')], parse(' x')); + + let s = 'x 3.14 "foo 10" z', + exp = [ + new Sexp(Symb, 'x'), + new Sexp(Num, 3.14), + new Sexp(Str, '"foo 10"'), + new Sexp(Symb, 'z'), + ], + res = parse(s); + // pr(exp); + // pr(res); + assert.deepEqual(exp, res); + + s = '(x 3.14) "foo 10" z'; + (exp = [ + new Sexp(List, [new Sexp(Symb, 'x'), new Sexp(Num, 3.14)]), + new Sexp(Str, '"foo 10"'), + new Sexp(Symb, 'z'), + ]), + (res = parse(s)); + pr(exp); + pr(res); + }); +}); + +const c = `ab + cd + fg + `; + +// const t = tokenizer('x "yz"'); +// pr(t.next()); +// pr(t.next()); +// pr(t.next()); diff --git a/misc/sexp/main.css b/misc/sexp/main.css new file mode 100644 index 0000000000..b3d4b7de5d --- /dev/null +++ b/misc/sexp/main.css @@ -0,0 +1,18 @@ +div.mycontent { + margin: 20px; +} + +.obj ul { + padding: 2px; +} + +.obj li { + display: flex; +} + +.array { + display: flex; +} +.colon { + margin: 0 10px; +} diff --git a/misc/sexp/main.html b/misc/sexp/main.html new file mode 100644 index 0000000000..5fd8143f01 --- /dev/null +++ b/misc/sexp/main.html @@ -0,0 +1,16 @@ + + misc + + +

letter count

+
+ +
not ready
+
+ + + + diff --git a/misc/sexp/s-parser.js b/misc/sexp/s-parser.js new file mode 100644 index 0000000000..d211fed2cf --- /dev/null +++ b/misc/sexp/s-parser.js @@ -0,0 +1,200 @@ +/** + * S-expression parser + * + * Recursive descent parser of a simplified sub-set of s-expressions. + * + * NOTE: the format of the programs is used in the "Essentials of interpretation" + * course: https://github.com/DmitrySoshnikov/Essentials-of-interpretation + * + * Grammar: + * + * s-exp : atom + * | list + * + * list : '(' list-entries ')' + * + * list-entries : s-exp list-entries + * | ε + * + * atom : symbol + * | number + * + * Examples: + * + * (+ 10 5) + * > ['+', 10, 5] + * + * (define (fact n) + * (if (= n 0) + * 1 + * (* n (fact (- n 1))))) + * + * > + * ['define', ['fact', 'n'], + * ['if', ['=', 'n', 0], + * 1, + * ['*', 'n', ['fact', ['-', 'n', 1]]]]] + * + * by Dmitry Soshnikov + * MIT Style License, 2016 + */ + +'use strict'; + +/** + * Parses a recursive s-expression into + * equivalent Array representation in JS. + */ +const SExpressionParser = { + parse(expression) { + this._expression = expression; + this._cursor = 0; + this._ast = []; + + return this._parseExpression(); + }, + + /** + * s-exp : atom + * | list + */ + _parseExpression() { + this._whitespace(); + + if (this._expression[this._cursor] === '(') { + return this._parseList(); + } + + return this._parseAtom(); + }, + + /** + * list : '(' list-entries ')' + */ + _parseList() { + // Allocate a new (sub-)list. + this._ast.push([]); + + this._expect('('); + this._parseListEntries(); + this._expect(')'); + + return this._ast[0]; + }, + + /** + * list-entries : s-exp list-entries + * | ε + */ + _parseListEntries() { + this._whitespace(); + + // ε + if (this._expression[this._cursor] === ')') { + return; + } + + // s-exp list-entries + + let entry = this._parseExpression(); + + if (entry !== '') { + // Lists may contain nested sub-lists. In case we have parsed a nested + // sub-list, it should be on top of the stack (see `_parseList` where we + // allocate a list and push it onto the stack). In this case we don't + // want to push the parsed entry to it (since it's itself), but instead + // pop it, and push to previous (parent) entry. + + if (Array.isArray(entry)) { + entry = this._ast.pop(); + } + + this._ast[this._ast.length - 1].push(entry); + } + + return this._parseListEntries(); + }, + + /** + * atom : symbol + * | number + */ + _parseAtom() { + const terminator = /\s+|\)/; + let atom = ''; + + while ( + this._expression[this._cursor] && + !terminator.test(this._expression[this._cursor]) + ) { + atom += this._expression[this._cursor]; + this._cursor++; + } + + if (atom !== '' && !isNaN(atom)) { + atom = Number(atom); + } + + return atom; + }, + + _whitespace() { + const ws = /^\s+/; + while ( + this._expression[this._cursor] && + ws.test(this._expression[this._cursor]) + ) { + this._cursor++; + } + }, + + _expect(c) { + if (this._expression[this._cursor] !== c) { + throw new Error( + `Unexpected token: ${this._expression[this._cursor]}, expected ${c}.`, + ); + } + this._cursor++; + }, +}; + +// ----------------------------------------------------------------------------- +// Tests + +const assert = require('assert'); + +function test(actual, expected) { + assert.deepEqual(SExpressionParser.parse(actual), expected); +} + +// Empty lists. +test(`()`, []); +test(`( )`, []); +test(`( ( ) )`, [[]]); + +// Simple atoms. +test(`1`, 1); +test(`foo`, 'foo'); + +// Non-empty and nested lists. +test(`(+ 1 15)`, ['+', 1, 15]); + +test(`(* (+ 1 15) (- 15 2))`, ['*', ['+', 1, 15], ['-', 15, 2]]); + +test( + ` + (define (fact n) + (if (= n 0) + 1 + (* n (fact (- n 1)))))`, + + [ + 'define', + ['fact', 'n'], + ['if', ['=', 'n', 0], 1, ['*', 'n', ['fact', ['-', 'n', 1]]]], + ], +); + +test(`(define foo 'test')`, ['define', 'foo', `'test'`]); + +console.info('All tests passed!'); diff --git a/misc/sexp/tests.html b/misc/sexp/tests.html new file mode 100644 index 0000000000..7b0e35db42 --- /dev/null +++ b/misc/sexp/tests.html @@ -0,0 +1,35 @@ + + + + + Mocha Tests + + + + + + +
+ + + + + + + + + + + + +
+
_1
+
_2
+
+ + diff --git a/misc/tests.html b/misc/tests.html new file mode 100644 index 0000000000..8c3b39213c --- /dev/null +++ b/misc/tests.html @@ -0,0 +1,32 @@ + + + + + Mocha Tests +  + + + + +
+ + + + + + + + + + + + diff --git a/misc/tortoise.png b/misc/tortoise.png new file mode 100644 index 0000000000..0f9b549d4f Binary files /dev/null and b/misc/tortoise.png differ diff --git a/mocha b/mocha new file mode 120000 index 0000000000..41d1b2cb93 --- /dev/null +++ b/mocha @@ -0,0 +1 @@ +../mocha/ \ No newline at end of file diff --git a/rxjs/.gitignore b/rxjs/.gitignore new file mode 100644 index 0000000000..b404e088a4 --- /dev/null +++ b/rxjs/.gitignore @@ -0,0 +1 @@ +smreactjs5* diff --git a/rxjs/blib/rxjs.umd.min.js b/rxjs/blib/rxjs.umd.min.js new file mode 100644 index 0000000000..fa58bf95d1 --- /dev/null +++ b/rxjs/blib/rxjs.umd.min.js @@ -0,0 +1,195 @@ +/** + @license + Apache License 2.0 https://github.com/ReactiveX/RxJS/blob/master/LICENSE.txt + **/ +/** + @license + Apache License 2.0 https://github.com/ReactiveX/RxJS/blob/master/LICENSE.txt + **/ +/* + ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. +*****************************************************************************/ +(function(g,y){"object"===typeof exports&&"undefined"!==typeof module?y(exports):"function"===typeof define&&define.amd?define("rxjs",["exports"],y):y(g.rxjs={})})(this,function(g){function y(b,a){function c(){this.constructor=b}if("function"!==typeof a&&null!==a)throw new TypeError("Class extends value "+String(a)+" is not a constructor or null");Ua(b,a);b.prototype=null===a?Object.create(a):(c.prototype=a.prototype,new c)}function Zd(b,a){var c={},d;for(d in b)Object.prototype.hasOwnProperty.call(b, +d)&&0>a.indexOf(d)&&(c[d]=b[d]);if(null!=b&&"function"===typeof Object.getOwnPropertySymbols){var e=0;for(d=Object.getOwnPropertySymbols(b);ea.indexOf(d[e])&&Object.prototype.propertyIsEnumerable.call(b,d[e])&&(c[d[e]]=b[d[e]])}return c}function $d(b,a,c,d){function e(a){return a instanceof c?a:new c(function(b){b(a)})}return new (c||(c=Promise))(function(c,h){function f(a){try{w(d.next(a))}catch(v){h(v)}}function k(a){try{w(d["throw"](a))}catch(v){h(v)}}function w(a){a.done?c(a.value): +e(a.value).then(f,k)}w((d=d.apply(b,a||[])).next())})}function Va(b,a){function c(a){return function(b){return d([a,b])}}function d(c){if(f)throw new TypeError("Generator is already executing.");for(;e;)try{if(f=1,h&&(l=c[0]&2?h["return"]:c[0]?h["throw"]||((l=h["return"])&&l.call(h),0):h.next)&&!(l=l.call(h,c[1])).done)return l;if(h=0,l)c=[c[0]&2,l.value];switch(c[0]){case 0:case 1:l=c;break;case 4:return e.label++,{value:c[1],done:!1};case 5:e.label++;h=c[1];c=[0];continue;case 7:c=e.ops.pop();e.trys.pop(); +continue;default:if(!(l=e.trys,l=0l[0]&&c[1]=b.length&&(b=void 0);return{value:b&&b[d++],done:!b}}};throw new TypeError(a?"Object is not iterable.":"Symbol.iterator is not defined.");}function x(b,a){var c="function"===typeof Symbol&&b[Symbol.iterator];if(!c)return b;b= +c.call(b);var d,e=[],f;try{for(;(void 0===a||0=b._refCount||0<--b._refCount)c=null;else{var d=b._connection,f=c;c=null;!d||f&&d!==f||d.unsubscribe();a.unsubscribe()}});b.subscribe(d);d.closed||(c=b.connect())})}function Mb(b){var a=T.schedule;return new p(function(c){var d=new C,e=b||Ea,f=e.now(),h=function(l){var k=e.now();c.next({timestamp:b?k:l,elapsed:k-f});c.closed||d.add(a(h))};d.add(a(h));return d})}function Nb(b){return b in Za?(delete Za[b],!0):!1}function de(b){return new p(function(a){return b.schedule(function(){return a.complete()})})} +function Fa(b){return b&&q(b.schedule)}function pa(b){return q(b[b.length-1])?b.pop():void 0}function N(b){return Fa(b[b.length-1])?b.pop():void 0}function Ob(b){return Symbol.asyncIterator&&q(null===b||void 0===b?void 0:b[Symbol.asyncIterator])}function Pb(b){return new TypeError("You provided "+(null!==b&&"object"===typeof b?"an invalid object":"'"+b+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Qb(b){return q(null=== +b||void 0===b?void 0:b[$a])}function Rb(b){return ae(this,arguments,function(){var a,c,d,e;return Va(this,function(f){switch(f.label){case 0:a=b.getReader(),f.label=1;case 1:f.trys.push([1,,9,10]),f.label=2;case 2:return[4,da(a.read())];case 3:return c=f.sent(),d=c.value,(e=c.done)?[4,da(void 0)]:[3,5];case 4:return[2,f.sent()];case 5:return[4,da(d)];case 6:return[4,f.sent()];case 7:return f.sent(),[3,2];case 8:return[3,10];case 9:return a.releaseLock(),[7];case 10:return[2]}})})}function t(b){if(b instanceof +p)return b;if(null!=b){if(q(b[qa]))return ee(b);if(ab(b))return fe(b);if(q(null===b||void 0===b?void 0:b.then))return ge(b);if(Ob(b))return Sb(b);if(Qb(b))return he(b);if(q(null===b||void 0===b?void 0:b.getReader))return Sb(Rb(b))}throw Pb(b);}function ee(b){return new p(function(a){var c=b[qa]();if(q(c.subscribe))return c.subscribe(a);throw new TypeError("Provided object does not correctly implement Symbol.observable");})}function fe(b){return new p(function(a){for(var c=0;ce&&(e=0);var h=0;return c.schedule(function(){a.closed||(a.next(h++),0<=d?this.schedule(void 0,d):a.complete())},e)})}function ec(b,a){void 0===b&&(b=0);void 0===a&&(a=I);0>b&&(b=0);return Z(b,b,a)}function aa(b){return 1===b.length&&we(b[0])?b[0]:b}function fc(){for(var b=[],a=0;a=b?function(){return K}:n(function(a,c){var d=0;a.subscribe(m(c,function(a){++d<=b&&(c.next(a),b<=d&&c.complete())}))})} +function nb(){return n(function(b,a){b.subscribe(m(a,D))})}function ob(b){return P(function(){return b})}function Na(b,a){return a?function(c){return ua(a.pipe(ha(1),nb()),c.pipe(Na(b)))}:H(function(a,d){return b(a,d).pipe(ha(1),ob(a))})}function xc(b,a){void 0===a&&(a=I);var c=Z(b,a);return Na(function(){return c})}function yc(){return n(function(b,a){b.subscribe(m(a,function(b){return Ga(b,a)}))})}function zc(b,a){return n(function(c,d){var e=new Set;c.subscribe(m(d,function(a){var c=b?b(a):a;e.has(c)|| +(e.add(c),d.next(a))}));null===a||void 0===a?void 0:a.subscribe(m(d,function(){return e.clear()},D))})}function pb(b,a){void 0===a&&(a=E);b=null!==b&&void 0!==b?b:De;return n(function(c,d){var e,f=!0;c.subscribe(m(d,function(c){var h=a(c);if(f||!b(e,h))f=!1,e=h,d.next(c)}))})}function De(b,a){return b===a}function Ac(b,a){return pb(function(c,d){return a?a(c[b],d[b]):c[b]===d[b]})}function wa(b){void 0===b&&(b=Ee);return n(function(a,c){var d=!1;a.subscribe(m(c,function(a){d=!0;c.next(a)},function(){return d? +c.complete():c.error(b())}))})}function Ee(){return new ba}function Bc(b,a){if(0>b)throw new qb;var c=2<=arguments.length;return function(d){return d.pipe(L(function(a,c){return c===b}),ha(1),c?va(a):wa(function(){return new qb}))}}function Cc(){for(var b=[],a=0;a(a||0)?Infinity:a;return n(function(d,e){return fb(d,e,b,a,void 0,!0,c)})}function Fc(b){return n(function(a, +c){try{a.subscribe(c)}finally{c.add(b)}})}function Gc(b,a){return n(Hc(b,a,"value"))}function Hc(b,a,c){var d="index"===c;return function(c,f){var e=0;c.subscribe(m(f,function(h){var l=e++;b.call(a,h,l,c)&&(f.next(d?l:h),f.complete())},function(){f.next(d?-1:void 0);f.complete()}))}}function Ic(b,a){return n(Hc(b,a,"index"))}function Jc(b,a){var c=2<=arguments.length;return function(d){return d.pipe(b?L(function(a,c){return b(a,c,d)}):E,ha(1),c?va(a):wa(function(){return new ba}))}}function Kc(b, +a,c,d){return n(function(e,f){function h(a,b){var c=new p(function(a){v++;var c=b.subscribe(a);return function(){c.unsubscribe();0===--v&&n&&Q.unsubscribe()}});c.key=a;return c}var l;a&&"function"!==typeof a?(c=a.duration,l=a.element,d=a.connector):l=a;var k=new Map,g=function(a){k.forEach(a);a(f)},r=function(a){return g(function(b){return b.error(a)})},v=0,n=!1,Q=new Lb(f,function(a){try{var e=b(a),g=k.get(e);if(!g){k.set(e,g=d?d():new B);var w=h(e,g);f.next(w);if(c){var v=m(g,function(){g.complete(); +null===v||void 0===v?void 0:v.unsubscribe()},void 0,void 0,function(){return k.delete(e)});Q.add(t(c(w)).subscribe(v))}}g.next(l?l(a):a)}catch(xe){r(xe)}},function(){return g(function(a){return a.complete()})},r,function(){return k.clear()},function(){n=!0;return 0===v});e.subscribe(Q)})}function Lc(){return n(function(b,a){b.subscribe(m(a,function(){a.next(!1);a.complete()},function(){a.next(!0);a.complete()}))})}function rb(b){return 0>=b?function(){return K}:n(function(a,c){var d=[];a.subscribe(m(c, +function(a){d.push(a);bb?a:b})}function Pc(b,a,c){void 0===c&&(c=Infinity);if(q(a))return H(function(){return b},a,c);"number"===typeof a&&(c=a);return H(function(){return b},c)}function Qc(b,a,c){void 0===c&&(c=Infinity);return n(function(d,e){var f=a;return fb(d,e,function(a,c){return b(f,a,c)},c,function(a){f=a},!1,void 0, +function(){return f=null})})}function Rc(){for(var b=[],a=0;ab(a,c)?a:c}:function(a,b){return a=c?function(){return K}:n(function(a,b){var e=0,f,k=function(){null===f||void 0===f?void 0:f.unsubscribe();f=null;if(null!=d){var a="number"===typeof d?Z(d):t(d(e)),c=m(b,function(){c.unsubscribe();g()});a.subscribe(c)}else g()},g=function(){var d=!1;f=a.subscribe(m(b,void 0,function(){++e=c?E:n(function(a,b){var f=0,h,g=function(){var l=!1;h=a.subscribe(m(b,function(a){e&&(f=0);b.next(a)},void 0,function(a){if(f++=b?E:n(function(a,c){var d=Array(b),e=0;a.subscribe(m(c,function(a){var f=e++;if(fe){null===(c=null=== +p||void 0===p?void 0:p.complete)||void 0===c?void 0:c.call(p);c=void 0;try{c=new xb(b,u,q,Od+"_"+b.type)}catch(ye){a.error(ye);return}a.next(c);a.complete()}else null===(d=null===p||void 0===p?void 0:p.error)||void 0===d?void 0:d.call(p,b),x(e)});e=q.user;d=q.method;h=q.async;e?u.open(d,k,h,e,q.password):u.open(d,k,h);h&&(u.timeout=q.timeout,u.responseType=q.responseType);"withCredentials"in u&&(u.withCredentials=q.withCredentials);for(r in f)f.hasOwnProperty(r)&&u.setRequestHeader(r,f[r]);c?u.send(c): +u.send();return function(){u&&4!==u.readyState&&u.abort()}})}function Oe(b,a){var c;if(!b||"string"===typeof b||"undefined"!==typeof FormData&&b instanceof FormData||"undefined"!==typeof URLSearchParams&&b instanceof URLSearchParams||Ab(b,"ArrayBuffer")||Ab(b,"File")||Ab(b,"Blob")||"undefined"!==typeof ReadableStream&&b instanceof ReadableStream)return b;if("undefined"!==typeof ArrayBuffer&&ArrayBuffer.isView(b))return b.buffer;if("object"===typeof b)return a["content-type"]=null!==(c=a["content-type"])&& +void 0!==c?c:"application/json;charset\x3dutf-8",JSON.stringify(b);throw new TypeError("Unknown body type");}function Ab(b,a){return Qe.call(b)==="[object "+a+"]"}var Ua=function(b,a){Ua=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(a,b){a.__proto__=b}||function(a,b){for(var c in b)Object.prototype.hasOwnProperty.call(b,c)&&(a[c]=b[c])};return Ua(b,a)},U=function(){U=Object.assign||function(b){for(var a,c=1,d=arguments.length;ca&&hb.index?1:-1:a.delay>b.delay? +1:-1};return a}(za),K=new p(function(b){return b.complete()}),ab=function(b){return b&&"number"===typeof b.length&&"function"!==typeof b},$a;$a="function"===typeof Symbol&&Symbol.iterator?Symbol.iterator:"@@iterator";(function(b){b.NEXT="N";b.ERROR="E";b.COMPLETE="C"})(g.NotificationKind||(g.NotificationKind={}));var Qa=function(){function b(a,b,d){this.kind=a;this.value=b;this.error=d;this.hasValue="N"===a}b.prototype.observe=function(a){return Ga(this,a)};b.prototype.do=function(a,b,d){var c=this.kind, +f=this.value,h=this.error;return"N"===c?null===a||void 0===a?void 0:a(f):"E"===c?null===b||void 0===b?void 0:b(h):null===d||void 0===d?void 0:d()};b.prototype.accept=function(a,b,d){return q(null===a||void 0===a?void 0:a.next)?this.observe(a):this.do(a,b,d)};b.prototype.toObservable=function(){var a=this.kind,b=this.value,d=this.error,b="N"===a?bb(b):"E"===a?Wb(function(){return d}):"C"===a?K:0;if(!b)throw new TypeError("Unexpected notification kind "+a);return b};b.createNext=function(a){return new b("N", +a)};b.createError=function(a){return new b("E",void 0,a)};b.createComplete=function(){return b.completeNotification};b.completeNotification=new b("C");return b}(),ba=R(function(b){return function(){b(this);this.name="EmptyError";this.message="no elements in sequence"}}),qb=R(function(b){return function(){b(this);this.name="ArgumentOutOfRangeError";this.message="argument out of range"}}),kd=R(function(b){return function(a){b(this);this.name="NotFoundError";this.message=a}}),jd=R(function(b){return function(a){b(this); +this.name="SequenceError";this.message=a}}),Xb=R(function(b){return function(a){void 0===a&&(a=null);b(this);this.message="Timeout has occurred";this.name="TimeoutError";this.info=a}}),le=Array.isArray,me=Array.isArray,ne=Object.getPrototypeOf,oe=Object.prototype,pe=Object.keys,$e={connector:function(){return new B},resetOnDisconnect:!0},te=["addListener","removeListener"],re=["addEventListener","removeEventListener"],ve=["on","off"],Vd=new p(D),we=Array.isArray,Ae=function(b,a){return b.push(a), +b},Ce={connector:function(){return new B}},wd={leading:!0,trailing:!1},Fe=function(){return function(b,a){this.value=b;this.interval=a}}(),af=Object.freeze({audit:jb,auditTime:ic,buffer:jc,bufferCount:kc,bufferTime:lc,bufferToggle:mc,bufferWhen:nc,catchError:kb,combineAll:Ka,combineLatestAll:Ka,combineLatest:mb,combineLatestWith:qc,concat:sc,concatAll:Ia,concatMap:La,concatMapTo:rc,concatWith:tc,connect:Ma,count:uc,debounce:vc,debounceTime:wc,defaultIfEmpty:va,delay:xc,delayWhen:Na,dematerialize:yc, +distinct:zc,distinctUntilChanged:pb,distinctUntilKeyChanged:Ac,elementAt:Bc,endWith:Cc,every:Dc,exhaust:Pa,exhaustAll:Pa,exhaustMap:Oa,expand:Ec,filter:L,finalize:Fc,find:Gc,findIndex:Ic,first:Jc,groupBy:Kc,ignoreElements:nb,isEmpty:Lc,last:Mc,map:P,mapTo:ob,materialize:Nc,max:Oc,merge:Rc,mergeAll:ta,flatMap:H,mergeMap:H,mergeMapTo:Pc,mergeScan:Qc,mergeWith:Sc,min:Tc,multicast:Ra,observeOn:ra,onErrorResumeNext:fc,pairwise:Uc,partition:function(b,a){return function(c){return[L(b,a)(c),L(gc(b,a))(c)]}}, +pluck:Vc,publish:Wc,publishBehavior:Xc,publishLast:Zc,publishReplay:$c,race:function(){for(var b=[],a=0;ak?new Ba(l):new Ba(l,k)};a.parseMarbles=function(a,b,e,f,g){var c=this;void 0===f&&(f=!1);void 0===g&&(g=!1);if(-1!==a.indexOf("!"))throw Error('conventional marble diagrams cannot have the unsubscription marker "!"');var d=z([],x(a)),h=d.length,m=[];a=g?a.replace(/^[ ]+/,"").indexOf("^"):a.indexOf("^");var n=-1===a?0:a*-this.frameTimeFactor,q="object"!==typeof b?function(a){return a}:function(a){return f&&b[a]instanceof Gb?b[a].messages:b[a]}, +p=-1;a=function(a){var b=n,f=function(a){b+=a*c.frameTimeFactor},h=void 0,k=d[a];switch(k){case " ":g||f(1);break;case "-":f(1);break;case "(":p=n;f(1);break;case ")":p=-1;f(1);break;case "|":h=ya;f(1);break;case "^":f(1);break;case "#":h=J("E",void 0,e||"error");f(1);break;default:if(g&&k.match(/^[0-9]$/)&&(0===a||" "===d[a-1])){var l=d.slice(a).join("").match(/^([0-9]+(?:\.[0-9]+)?)(ms|s|m) /);if(l){a+=l[0].length-1;var k=parseFloat(l[1]),r=void 0;switch(l[2]){case "ms":r=k;break;case "s":r=1E3* +k;break;case "m":r=6E4*k}f(r/u.frameTimeFactor);break}}h=J("N",q(k),void 0);f(1)}h&&m.push({frame:-1=a)return K;var d=a+b;return new p(c?function(a){var e=b;return c.schedule(function(){e x % 2 === 1), +// map((x) => x + x), +// ) +// .subscribe((x) => pr(x)); + +class ClickStream { + constructor(element, cb = (e) => [e]) { + this.element = element; + this.cb = cb; + this.cb0 = false; + element.addEventListener('click', (e) => this.cb(e)); + } + + subscribe(cb) { + const cb0 = this.cb; + this.cb = (e) => cb0(e).map(cb); + return this; + } + + filter(pred) { + const cb0 = this.cb; + this.cb = (e) => cb0(e).filter(pred); + return this; + } + + map(fn) { + const cb0 = this.cb; + this.cb = (e) => cb0(e).map(fn); + return this; + } + + debounce(period) { + const cb0 = this.cb; + this.cb = (e) => { + if (this.cb0) return; + const res = cb0(e); + this.cb0 = this.cb; + this.cb = () => {}; + setTimeout(() => { + this.cb = this.cb0; + this.cb0 = null; + }, period); + return res; + }; + return this; + } +} + +// run it +export function runClickStreamExample() { + const div = document.querySelector('.mycontent'); + + const cs = new ClickStream(div); + cs.map((e) => [e.clientX, e.clientY]) + .filter(([x, y]) => x != y) + .subscribe((e) => { + pr('subscribe 0, e', e); + return e; + }) + .debounce(1000) + .map(([x, y]) => x + y) + .subscribe((e) => { + pr('subscribe 1, e', e); + return e; + }); + + let clicks = [ + [10, 20], + [0, 0], + [3, 5], + [5, 5], + ].forEach(([x, y]) => + div.dispatchEvent(new MouseEvent('click', { clientX: x, clientY: y })), + ); +} + +export { ClickStream }; diff --git a/rxjs/html/click-stream/clicks.html b/rxjs/html/click-stream/clicks.html new file mode 100644 index 0000000000..ba79b16f3a --- /dev/null +++ b/rxjs/html/click-stream/clicks.html @@ -0,0 +1,17 @@ + + + + + Clicks stream + + + + + +
+
click me a few times
+
+ + + + diff --git a/rxjs/html/click-stream/main.css b/rxjs/html/click-stream/main.css new file mode 100644 index 0000000000..e40affcbb5 --- /dev/null +++ b/rxjs/html/click-stream/main.css @@ -0,0 +1,9 @@ +.content { +} + +#square { + margin: 1em; + height: 100px; + width: 100px; + background: grey; +} diff --git a/rxjs/html/inventory/components.js b/rxjs/html/inventory/components.js new file mode 100644 index 0000000000..2d26512583 --- /dev/null +++ b/rxjs/html/inventory/components.js @@ -0,0 +1,84 @@ +// Utilitis +const pr = console.table; +const Element = (tag) => document.createElement(tag); + +// {tools, ...[onChangeFn]} -> Element('ul') +export function Suggestions({ tools, dragstart }) { + pr('Suggestions tools:', tools); + // TODO: change for iterator + const lst = [...tools].map(({ num }) => { + const li = Element('li'), + a = Element('a'); + // c = Element('span'), + // s = Element('span'), + // del = Element('button'), + // ed = Element('button'), + // sbtn = Element('span'); + a.append(`tool-${num}`); + a.href = `tool-${num}`; + a.draggable = true; + a.id = num; + a.addEventListener('dragstart', dragstart); + li.append(a); + // c.append(city); + // s.append(st); + // del.append('Del'); + // del.addEventListener('click', () => { + // onDel(city, st); + // }); + // ed.append('Edit'); + // ed.addEventListener('click', () => { + // onEdit(city, st); + // }); + // sbtn.append(ed, del); + // li.append(c, s, sbtn); + return li; + }); + const ul = Element('ul'); + ul.append(...lst); + return ul; +} +// ========== + +// const suggestions = document.querySelector('.suggestions'); +// // str, Element -> +// function showSuggestions(pattern, rootEl) { +// const ps = App.places(pattern), +// lis = placesList({ +// places: ps, +// onDel: (city, st) => App.delCity(city, st), +// onEdit: (city, st) => App.editCity(city, st), +// }); +// rootEl.replaceChildren(...lis); +// } + +// App.eReg.listen('SearchChange', ({ pattern }) => +// showSuggestions(pattern, suggestions), +// ); + +// App.eReg.listen('PlacesChange', () => { +// const pattern = search.value; +// showSuggestions(pattern, suggestions); +// }); + +// showSuggestions('', suggestions); +// ========== + +// const search = document.querySelector('.search'); +// search.addEventListener('keyup', (e) => { +// const event = { name: 'SearchChange', pattern: e.target.value }; +// App.eReg.fireEvent(event); +// }); +// // ========== + +// const add = document.querySelector('.add'); +// add.addEventListener('change', (e) => { +// const { place, err } = App.addCity(e.target.value); +// if (err) { +// pr('Error', err); +// return; +// } +// const event = { name: 'PlacesChange', place: place }; +// App.eReg.fireEvent(event); +// e.target.value = ''; +// }); diff --git a/rxjs/html/inventory/handlers.js b/rxjs/html/inventory/handlers.js new file mode 100644 index 0000000000..5c7368a322 --- /dev/null +++ b/rxjs/html/inventory/handlers.js @@ -0,0 +1,58 @@ +const pr = console.log; +import { Suggestions } from './components.js'; + +function drop(e, data) { + e.preventDefault(); + // pr('in drop', e); + // pr('in drop clientX', e.clientX); + const id = e.dataTransfer.getData('text'), + a = document.getElementById(id); + + // place Element + const x = e.clientX, + y = e.clientY; + a.style.position = 'absolute'; + a.style.left = `${x}px`; + a.style.top = `${y}px`; + e.target.append(a); + + // update store + const tool = data.get(id); + tool.x = x; + tool.y = y; + tool.picId = e.target.id; + pr('tool', tool); + // pr('data', data); +} + +function dragstart(e) { + pr(e); + e.dataTransfer.setData('text', e.target.id); +} + +function initData() { + const m = new Map(); + for (let i = 1; i <= 3; i++) { + m.set(i.toString(), { num: i }); + } + return m; +} +const data = initData(); + +pr(data); + +// init DOM +const pic = document.querySelector('#pic'); +pic.addEventListener('drop', (e) => drop(e, data)); +pic.addEventListener('dragover', (e) => { + e.preventDefault(); +}); + +const tools = document.querySelector('#tools'), + toolList = Suggestions({ tools: data.values(), dragstart: dragstart }); +tools.append(toolList); + +pr(tools); + +// pr(pic); +// window.drop = drop; diff --git a/rxjs/html/inventory/inventory.css b/rxjs/html/inventory/inventory.css new file mode 100644 index 0000000000..af72ab2627 --- /dev/null +++ b/rxjs/html/inventory/inventory.css @@ -0,0 +1,30 @@ +div.pics { + margin: 20px; +} +#pic { + height: 200px; + width: 200px; + background: lightgrey; + border: dotted black; +} +#inpic { + /* height: 20px; */ + /* width: 20px; */ + margin: 5px; + border: solid black; + position: absolute; + top: 180px; + left: 15px; +} + +div.tools { + margin: 20px; + border: dotted black; + width: 200px; +} + +/* .picture { */ +/* width: 50%; */ +/* background: url('./pics/01.jpg') bottom center; */ +/* background-size: cover; */ +/* } */ diff --git a/rxjs/html/inventory/inventory.spec.js b/rxjs/html/inventory/inventory.spec.js new file mode 100644 index 0000000000..58d4bdec0d --- /dev/null +++ b/rxjs/html/inventory/inventory.spec.js @@ -0,0 +1,19 @@ +const pr = console.log; +let { from, of, range, filter, map, Observable, Subscribe } = rxjs; +const assert = chai.assert; + +describe('misc', () => { + it('hi', () => { + pr('hi'); + }); +}); + +let pic = document.querySelector('#pic'); +pr(pic); +pr(window); +// window.dropH = (e) => pr('dropH', e); +// window.dragoverH = (e) => {}; +// pic.ondrop = (e) => pr(e); +// pic.ondragover = (e) => {}; +// pic.setAttribute('ondrop', dropH); +// pic.setAttribute('ondragover', dragoverH); diff --git a/rxjs/html/inventory/pics/01.jpg b/rxjs/html/inventory/pics/01.jpg new file mode 100644 index 0000000000..590036d636 Binary files /dev/null and b/rxjs/html/inventory/pics/01.jpg differ diff --git a/rxjs/html/inventory/router.html b/rxjs/html/inventory/router.html new file mode 100644 index 0000000000..c5d25220e9 --- /dev/null +++ b/rxjs/html/inventory/router.html @@ -0,0 +1,14 @@ + + + + + Try drag and drop + + + +
+
Tools
+ +
+ + diff --git a/rxjs/html/inventory/router.js b/rxjs/html/inventory/router.js new file mode 100644 index 0000000000..7dad8cc1e8 --- /dev/null +++ b/rxjs/html/inventory/router.js @@ -0,0 +1,58 @@ +// Utilitis +const pr = console.table; +const Element = (tag) => document.createElement(tag); + +// [Link] -> Element('div header') +export function Header(...links) { + const h = Element('div'); + h.append(...links); + return h; +} +// str -> Element('a') +export function Link(path) { + const l = Element('a'); + l.href = path; + l.onclick = (e) => { + e.preventDefault(); + pr('in link', path); + }; + return l; +} + +// {tools, ...[onChangeFn]} -> Element('ul') +export function Suggestions({ tools, dragstart }) { + pr('Suggestions tools:', tools); + // TODO: change for iterator + const lst = [...tools].map(({ num }) => { + const li = Element('li'), + a = Element('a'); + // c = Element('span'), + // s = Element('span'), + // del = Element('button'), + // ed = Element('button'), + // sbtn = Element('span'); + a.append(`tool-${num}`); + a.href = `tool-${num}`; + a.draggable = true; + a.id = num; + a.addEventListener('dragstart', dragstart); + li.append(a); + // c.append(city); + // s.append(st); + // del.append('Del'); + // del.addEventListener('click', () => { + // onDel(city, st); + // }); + // ed.append('Edit'); + // ed.addEventListener('click', () => { + // onEdit(city, st); + // }); + // sbtn.append(ed, del); + // li.append(c, s, sbtn); + return li; + }); + const ul = Element('ul'); + ul.append(...lst); + return ul; +} +// ========== diff --git a/rxjs/html/inventory/test.html b/rxjs/html/inventory/test.html new file mode 100644 index 0000000000..a75d03f9a3 --- /dev/null +++ b/rxjs/html/inventory/test.html @@ -0,0 +1,31 @@ + + + + + Mocha Tests + + + + +
+ + + + + + + + + + + + + + + + diff --git a/rxjs/html/inventory/try.html b/rxjs/html/inventory/try.html new file mode 100644 index 0000000000..0f6d4d0c7f --- /dev/null +++ b/rxjs/html/inventory/try.html @@ -0,0 +1,20 @@ + + + + + Try drag and drop + + + +
+
+
+ Shop wall pic +
text
+
+
+
Tools
+ +
+ + diff --git a/rxjs/html/router/t.css b/rxjs/html/router/t.css new file mode 100644 index 0000000000..f7acf77db0 --- /dev/null +++ b/rxjs/html/router/t.css @@ -0,0 +1,3 @@ +a { + margin: 40px; +} diff --git a/rxjs/html/router/t.html b/rxjs/html/router/t.html new file mode 100644 index 0000000000..e076f97041 --- /dev/null +++ b/rxjs/html/router/t.html @@ -0,0 +1,13 @@ + + + + + Routing + + + + +
+ + + diff --git a/rxjs/html/router/t.js b/rxjs/html/router/t.js new file mode 100644 index 0000000000..55d0b83250 --- /dev/null +++ b/rxjs/html/router/t.js @@ -0,0 +1,159 @@ +// Utilitis +const pr = console.log; +prRet = (text, val) => { + pr(text, val); + return val; +}; +const Element = (tag, props) => { + const e = document.createElement(tag); + Object.assign(e, props); + return e; +}; + +// app state +//==================== +const get_State = () => { + if (!globalThis._GState) globalThis._GState = {}; + return globalThis._GState; +}; +const get_Router = () => get_State().router; +const set_Router = (router) => (get_State().router = router); + +// components +//==================== + +// str, str -> DOM +function Link(path, text) { + const a = Element('a', { + href: path, + textContent: text, + onclick: (e) => { + e.preventDefault(); + // pr('in a, e', e); + // pr('path', path); + get_Router().go(path); + }, + }); + // a.textContent = text; + return a; +} + +class Router { + // routes: [{path, params, el()}], Router, DOM node + constructor({ routes, parent = get_Router(), root }) { + this.routes = Router.makeRoutes(routes); + this.parent = parent; + this.root = root; + } + + // routes: [{path, params, el()}] -> Map[path-> {params: params, el: Element()}] + static makeRoutes(routes) { + return routes.reduce( + (acc, { path, params, el }) => acc.set(path, { params, el }), + new Map(), + ); + } + + static getPathParams(path0) { + let [path, params] = path0.split('?'); + if (!params) return { path: path }; + params = params + .split('&') + .map((x) => x.split('=')) + .reduce((acc, [k, v]) => { + acc[k] = v; + return acc; + }, {}); + pr('param after', params); + return { path: path, params: params }; + } + + // str -> * DOM change + go(path) { + // set as globalThis router + set_Router(this); + // TODO: check path for / and .. // + if (path.startsWith('/')) return this.parent.go(path); + if (path.startsWith('../')) return this.parent.go(path.slice(3)); + this.go_local(path); + } + + // str -> * DOM change + go_local(path0) { + const { path, params } = Router.getPathParams(path0), + { root, params0, el } = this.routes.get(path); + const params1 = Object.assign(params0 || {}, params), + child = el(params1); + this.root.replaceChildren(child); + } +} + +// run app +//==================== +function run(root) { + const as = [ + Link('home', 'home'), + Link('some', 'go there'), + Link('', 'home too'), + Link('accnt?name=chb&lastname=foo', 'chb accnt'), + Link('friends?name=andron&lastname=gorb', 'friends'), + ], + header = document.querySelector('#header'); + header.append(...as); + + const home = () => { + const d = Element('div'); + d.append(Element('h2', { textContent: 'Home' })); + d.append(Element('p', { textContent: 'my home page rocks' })); + return d; + }; + + const accnt = ({ name, lastname }) => { + const d = Element('div'); + d.append(Element('h2', { textContent: `Hello ${name}` })); + d.append( + Element('p', { textContent: `your full name: ${name} ${lastname}` }), + ); + return d; + }; + + const router = new Router({ + root: document.querySelector('#content'), + routes: [ + { path: 'home', el: home }, + { path: 'some', el: () => Element('h3', { textContent: 'some page' }) }, + { path: '', el: home }, + { path: 'accnt', el: accnt }, + { path: 'friends', el: Friends }, + ], + }); + + // router.go('home'); + router.go('accnt?name=vlad&lastname=chb'); + + function Friends({ name }) { + const d = Element('div'); + d.append(Element('h2', { textContent: `Hello ${name}` })); + d.append(Element('p', { textContent: `your full name: ${name}` })); + const as = [ + Link('../', 'back'), + Link('some', 'some'), + Link('friends?name=vlad', 'vlad'), + ]; + d.append(...as); + + const router = new Router({ + root: document.querySelector('#content'), + routes: [ + { path: '', el: home }, + { path: 'some', params: { name: 'some' }, el: Friends }, + { path: 'friends', el: Friends }, + ], + }); + router.go(''); + + return d; + } +} + +run(); diff --git a/rxjs/html/starship/enemy.js b/rxjs/html/starship/enemy.js new file mode 100644 index 0000000000..b1e54d9faf --- /dev/null +++ b/rxjs/html/starship/enemy.js @@ -0,0 +1,76 @@ +const ENEMY_SIZE = 20, + ENEMY_FREQUENCY = 1500, + ENEMY_SPEED_Y = 4, + ENEMY_COLORS = ['red', 'blue', 'gree'], + ENEMY_SHOTS_FEQ = 1500; + +const randColorE = () => ENEMY_COLORS[randInt(0, ENEMY_COLORS.length)]; + +const enemyStream = interval(ENEMY_FREQUENCY).pipe( + scan((acc, i) => { + const x = randX(), + y = -15, + color = randColorE(), + shots = [], + enemy = { x, y, color, shots }; + + // shooting + interval(ENEMY_SHOTS_FEQ).subscribe(() => { + enemy.shots.push({ x: enemy.x, y: enemy.y }); + enemy.shots.filter(isVisible); + }); + + acc.push(enemy); + return acc + .filter(isVisible) + .filter((e) => !(e.isDead && e.shots.length < 1)); + }, []), + // map((arr) => prRet(arr)), + // map((arr) => arr.filter(isVisible)), +); + +function paintEenemyShots(shots) { + paintShots(shots, SHOT_SPEED, '#00ff00', 'down'); +} + +function paintEnemy(es) { + es.forEach((e) => { + e.y += ENEMY_SPEED_Y; + e.x += randInt(-15, 15); + if (!e.isDead) drawTriangle(e.x, e.y, ENEMY_SIZE, e.color, 'down'); + paintEenemyShots(e.shots); + }); +} + +function isVisible({ x, y }) { + // pr(x, y); + return x > -40 && x < canvas.width + 40 && y > -40 && y < canvas.height + 40; +} + +function collision(a, b) { + const r = 20; + return Math.abs(a.x - b.x) < r && Math.abs(a.y - b.y) < r; +} + +// [{x,y}], {x,y} -> * mutated enemy +function heroShotsToEnemy(shots, enemies) { + for (const enemy of enemies) { + for (const shot of shots) { + if (collision(shot, enemy)) { + enemy.isDead = true; + setTimeout(() => (enemy.y = -100), enemyTTL(enemy)); + break; + } + } + } +} + +function enemyTTL({ x, y }) { + return (canvas.height - y) / ENEMY_SPEED_Y / 40; +} + +function gameOver(hero, enemies) { + return enemies.some( + (e) => collision(hero, e) || e.shots.some((s) => collision(hero, s)), + ); +} diff --git a/rxjs/html/starship/hero.js b/rxjs/html/starship/hero.js new file mode 100644 index 0000000000..79069f6e4b --- /dev/null +++ b/rxjs/html/starship/hero.js @@ -0,0 +1,25 @@ +const HERO_SIZE = 20; + +const HERO_Y = canvas.height - 30, + mouseMove = fromEvent(canvas, 'mousemove'), + spaceShip = mouseMove.pipe( + map((event) => ({ x: event.clientX, y: HERO_Y })), + startWith({ + x: canvas.width / 2, + y: HERO_Y, + }), + ); + +function drawTriangle(x, y, width, color, direction) { + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(x - width, y); + ctx.lineTo(x, direction === 'up' ? y - width : y + width); + ctx.lineTo(x + width, y); + ctx.lineTo(x - width, y); + ctx.fill(); +} + +function paintSpaceShip({ x, y }) { + drawTriangle(x, y, HERO_SIZE, '#ff0000', 'up'); +} diff --git a/rxjs/html/starship/main.html b/rxjs/html/starship/main.html new file mode 100644 index 0000000000..6dd2739903 --- /dev/null +++ b/rxjs/html/starship/main.html @@ -0,0 +1,20 @@ + + + + Starship shooter + + + + + + + + + + diff --git a/rxjs/html/starship/main.js b/rxjs/html/starship/main.js new file mode 100644 index 0000000000..4da88d01ec --- /dev/null +++ b/rxjs/html/starship/main.js @@ -0,0 +1,94 @@ +let { + BehaviorSubject, + takeWhile, + distinctUntilChanged, + distinctUntilKeyChanged, + timestamp, + merge, + sampleTime, + scan, + combineLatest, + startWith, + fromEvent, + flatMap, + mergeMap, + toArray, + take, + interval, + from, + of, + range, + filter, + map, + Observable, + Subscribe, +} = rxjs; + +// global const +const pr = console.log, + prRet = (...xs) => { + pr(...xs); + return xs[xs.length - 1]; + }; +(Element = (tag) => document.createElement(tag)), + (SPEED = 40), + (STAR_NUMBER = 250); + +// random utilities =============== +const randInt = (i, j) => parseInt(i + Math.random() * (j - i), 10), + randX = () => randInt(0, canvas.width), + randY = () => randInt(0, canvas.height), + randColor = () => '#' + randInt(0, parseInt('999999', 16)).toString(16); + +// set up canvas ===== +const canvas = Element('canvas'), + ctx = canvas.getContext('2d'); +canvas.width = window.innerWidth; +canvas.height = window.innerHeight; +document.body.append(canvas); + +// render ===== +function renderScene({ stars, hero, enemies, hshots, score }) { + paintStars(stars); + paintSpaceShip(hero); + paintEnemy(enemies); + paintHeroShots(hshots, enemies); + paintScore(score); +} + +// main ===== +function main() { + const Game = combineLatest([ + starStream, + spaceShip, + enemyStream, + heroShots, + score, + ]).pipe( + sampleTime(SPEED), + takeWhile((o) => { + let [stars, hero, enemies, hshots, score] = o; + return !gameOver(hero, enemies); + }), + // map((o) => { + // let [stars, hero, enemies, hshots] = o; + // heroShotsToEnemy(hshots, enemies); + // return o; + // }), + ); + + const run = () => { + Game.subscribe(([stars, hero, enemies, hshots, score]) => + renderScene({ stars, hero, enemies, hshots, score }), + ); + }; + run(); +} + +// export module ===== +// const MM = { +// main: () => { +// pr('hi there'); +// main(); +// }, +// }; diff --git a/rxjs/html/starship/score.js b/rxjs/html/starship/score.js new file mode 100644 index 0000000000..719049a08c --- /dev/null +++ b/rxjs/html/starship/score.js @@ -0,0 +1,8 @@ +function paintScore(score) { + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 26px sans-serif'; + ctx.fillText(`Score: ${score}`, 40, 43); +} + +const scoreSubject = new BehaviorSubject(0), + score = scoreSubject.pipe(scan((prev, cur) => prev + cur, 0)); diff --git a/rxjs/html/starship/shots.js b/rxjs/html/starship/shots.js new file mode 100644 index 0000000000..3b102e8a7d --- /dev/null +++ b/rxjs/html/starship/shots.js @@ -0,0 +1,51 @@ +const SHOT_SPEED = 15, + FIRE_FREQ = 200, + SCORE_INC = 10; + +const playerFiring = merge( + // fromEvent(canvas, 'click'), + fromEvent(document, 'keydown').pipe( + filter((e) => e.keyCode == 32 || e.key == ' '), + ), +).pipe(startWith({}), sampleTime(FIRE_FREQ), timestamp()); + +const heroShots = combineLatest([playerFiring, spaceShip]).pipe( + map(([fire, ship]) => ({ + x: ship.x, + timestamp: fire.timestamp, + })), + + distinctUntilChanged(undefined, (shot) => shot.timestamp), + // distinctUntilKeyChanged('timestamp'), + + scan((acc, shot) => { + acc.push({ + x: shot.x, + y: HERO_Y, + }); + return acc.filter(isVisible); + }, []), +); + +function paintShots(shots, speed, color, direction) { + shots.forEach((s) => { + s.y += speed; + drawTriangle(s.x, s.y, 5, color, direction); + }); +} + +function paintHeroShots(shots, enemies) { + shots.forEach((s) => { + for (const e of enemies) { + if (!e.isDead && collision(s, e)) { + scoreSubject.next(SCORE_INC); + e.isDead = true; + s.y = -1000; + break; + } + } + + s.y -= SHOT_SPEED; + drawTriangle(s.x, s.y, 5, '#ffff00', 'up'); + }); +} diff --git a/rxjs/html/starship/starfield.js b/rxjs/html/starship/starfield.js new file mode 100644 index 0000000000..803d3fa664 --- /dev/null +++ b/rxjs/html/starship/starfield.js @@ -0,0 +1,26 @@ +function paintStars(stars) { + ctx.fillStyle = '#000000'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#ffffff'; + stars.forEach((s) => ctx.fillRect(s.x, s.y, s.size, s.size)); +} + +const starStream = range(STAR_NUMBER).pipe( + map(() => ({ + x: randX(), + y: randY(), + size: Math.random() * 3 + 1, + })), + toArray(), + mergeMap((arr) => + interval(SPEED).pipe( + map((i) => { + arr.forEach((s) => { + if (s.y >= canvas.height) s.y = 0; + s.y += s.size; + }); + return arr; + }), + ), + ), +); diff --git a/rxjs/html/test/concurrent0.spec.js b/rxjs/html/test/concurrent0.spec.js new file mode 100644 index 0000000000..32f51a692d --- /dev/null +++ b/rxjs/html/test/concurrent0.spec.js @@ -0,0 +1,97 @@ +const pr = console.log; +let { + take, + scan, + reduce, + from, + of, + range, + filter, + map, + Observable, + Subscribe, + ReplaySubject, + Subject, +} = rxjs; +const assert = chai.assert; + +describe('reading the book', () => { + it('count evens with reduce', () => { + let got; + range(21) + .pipe(reduce((acc, x) => (x % 2 === 0 ? acc + 1 : acc))) + .subscribe((x) => (got = x)); + assert.equal(got, 10); + }); + + it('count evens with scan, from "infinite" stream', () => { + let got, + source = range(20000).pipe( + scan((acc, x) => (x % 2 === 0 ? acc + 1 : acc)), + take(21), + ); + source.subscribe((x) => (got = x)); + assert.equal(got, 10); + // again + source.subscribe((x) => (got = x)); + assert.equal(got, 10); + }); + + it('Subject', () => { + let subj = new Subject(), + acc = []; + subj.next(1); + subj.next(2); + subj.subscribe((x) => acc.push(x)); + subj.next(3); + assert.deepEqual([3], acc); + }); + + it('ReplaySubject', () => { + let subj = new ReplaySubject(), + acc = []; + subj.next(1); + subj.next(2); + subj.subscribe((x) => acc.push(x)); + subj.next(3); + assert.deepEqual([1, 2, 3], acc); + }); +}); + +describe('my promise', () => { + let prOf1; + beforeEach( + () => + (prOf1 = new Promise((resolve, reject) => setTimeout(resolve(1), 30))), + ); + + it('promise w/ done() called', (done) => { + prOf1.then((res) => { + assert.equal(1, res); + done(); + return 2; + }); + }); + + it('return promise w/o using done()', () => { + return prOf1.then((res) => { + assert.equal(1, res); + return res * 2; + }); + }); + + it('if chain of then return promise also', () => { + return prOf1 + .then((res) => { + assert.equal(1, res); + return res * 2; + }) + .then((res) => { + assert.equal(2, res); + return res * 2; + }) + .then((res) => { + assert.equal(4, res); + }); + }); +}); diff --git a/rxjs/html/test/earthquake.html b/rxjs/html/test/earthquake.html new file mode 100644 index 0000000000..cd360b2e56 --- /dev/null +++ b/rxjs/html/test/earthquake.html @@ -0,0 +1,27 @@ + + + + +

Screen

+
+ +
+ System Requirements +

+ Requires a computer running an operating system. The computer must have + some memory and ideally some kind of long-term storage. An input device + as well as some form of output device is recommended. +

+
+ + + + diff --git a/rxjs/html/test/earthquake.js b/rxjs/html/test/earthquake.js new file mode 100644 index 0000000000..4cf6443aef --- /dev/null +++ b/rxjs/html/test/earthquake.js @@ -0,0 +1,74 @@ +const pr = console.log; +let { take, interval, from, of, range, filter, map, Observable, Subscribe } = + rxjs; + +const randInt = (n) => Math.floor(Math.random() * n); +const randIntIJ = (i, j) => i + Math.floor(Math.random() * (j - i)); + +const width = 100, + height = 100, + n = 3, + period = 1000; + +// components ==================== +// const Screen = ({ width, height, n, period }) => { +// const elm = document.createElement('div'); +// elm.style = `width:${width}px; height:${height}px; border: solid black`; +// return elm; +// }; + +const Spot = ({ left, top, size, color }) => { + const elm = document.createElement('div'); + elm.style = `width:${size}px; height:${size}px; border: solid ${color}; + border-radius:50%; + position: absolute; + left: ${left}px; + top: ${top}px;`; + return elm; +}; + +function randRemoveSpot(parent) { + const spots = parent.children; + // pr('spot', spot); + if (spots.length > 2) parent.removeChild(spots[randInt(spots.length)]); +} + +// run ==================== + +const run = () => { + const scr = document.querySelector('#screen'); + window.scr = scr; + + // values in px + let size = 20, + // left = () => randIntIJ(0, 100 - size), + // top = (i) => randIntIJ(-i * size, 100 - (i + 1) * size), + left = () => + randIntIJ(scr.offsetLeft, scr.offsetWidth - scr.offsetLeft - size), + top = () => randIntIJ(scr.offsetTop, scr.offsetHeight + size), + // randIntIJ(scr.offsetTop, scr.offsetHeight - scr.offsetTop - size), + colors = ['red', 'blue', 'yellow', 'black', 'green', 'purple'], + randColor = () => colors[randInt(colors.length)]; + + const makeSpot = () => { + const props = { + left: left(), + top: top(), + size: size, + color: randColor(), + }; + // pr('props', props); + const spot = Spot(props); + return spot; + }; + + interval(100) + .pipe(map(makeSpot), take(100)) + .subscribe((spot) => scr.append(spot)); + + interval(150) + .pipe(take(100)) + .subscribe(() => randRemoveSpot(scr)); +}; + +const MM = { randIntIJ, Spot, run }; diff --git a/rxjs/html/test/earthquake.spec.js b/rxjs/html/test/earthquake.spec.js new file mode 100644 index 0000000000..52f9ca4673 --- /dev/null +++ b/rxjs/html/test/earthquake.spec.js @@ -0,0 +1,26 @@ +// let { from, of, range, filter, map, Observable, Subscribe } = rxjs; +const assert = chai.assert; + +describe('misc', () => { + it('randInt', () => { + let a = [...Array(5)].map((x) => randInt(5)), + exp = a.every((x) => x >= 0 && x < 5); + // pr('a', a); + assert.ok(exp); + }); + it('randIntIJ', () => { + let a = [...Array(5)].map((x) => randIntIJ(2, 5)), + exp = a.every((x) => x >= 2 && x < 5); + // pr('a', a); + assert.ok(exp); + + a = [...Array(5)].map((x) => randIntIJ(-5, -10)); + exp = a.every((x) => x >= -10 && x < -5); + assert.ok(exp); + + a = [...Array(5)].map((x) => randIntIJ(-5, 10)); + exp = a.every((x) => x >= -5 && x < 10); + pr('a', a); + assert.ok(exp); + }); +}); diff --git a/rxjs/html/test/mocha b/rxjs/html/test/mocha new file mode 120000 index 0000000000..0ad8d91655 --- /dev/null +++ b/rxjs/html/test/mocha @@ -0,0 +1 @@ +../../../mocha \ No newline at end of file diff --git a/rxjs/html/test/myobservable.spec.js b/rxjs/html/test/myobservable.spec.js new file mode 100644 index 0000000000..e2a4a0ddfe --- /dev/null +++ b/rxjs/html/test/myobservable.spec.js @@ -0,0 +1,160 @@ +const pr = console.log; +let { from, of, range, filter, map, Observable, Subscribe } = rxjs; +const assert = chai.assert; +// import { ClickStream } from '../click-stream/click-stream.js'; + +// range(1, 1) +// .compose( +// filter((x) => x % 2 === 1), +// map((x) => x + x), +// ) +// .subscribe((x) => pr(x)); + +const identity = (x) => x; + +// window.myId = identity; + +const compose = + (...fns) => + (arg) => + fns.reduce((acc, fn) => fn.call(null, acc), arg); + +describe('compose', () => { + it('array.reduce', () => { + let a = [1, 2, 3], + got = a.reduce((acc, x) => { + acc.push(x); + return acc; + }, []); + assert.deepEqual(a, got); + }); + + it('basic use', () => { + let got = compose(identity)(1); + assert.equal(1, got); + + got = compose( + identity, + (x) => x * 3, + (x) => x * 2, + identity, + )(1); + assert.equal(1 * 2 * 3, got); + + got = compose( + (xs) => xs.map((x) => x * 2), + (xs) => xs.filter((x) => x > 5), + )([1, 2, 3, 4]); + assert.deepEqual([6, 8], got); + }); +}); + +// ==================== +// Implementation of Observable of rxjs +class MyEvent { + constructor(e, date = new Date()) { + this.e = e; + this.date = date; + } +} + +class MyObservable { + constructor(cb) { + this.cb = cb; + } + + subscribe(cb) { + const cb0 = this.cb; + this.cb = (e) => { + cb0(e).map(cb); + return [e]; + }; + return this; + } + + map(cb) { + const cb0 = this.cb; + this.cb = (e) => cb0(e).map(cb); + return this; + } + + filter(cb) { + const cb0 = this.cb; + this.cb = (e) => cb0(e).filter(cb); + return this; + } + + take(n) { + const cb0 = this.cb; + this.cb = (e) => { + n--; + if (n > 0) return cb0(e); + else return []; + }; + return this; + } + + debounce(period) { + // const cb0 = this.cb; + // this.cb = (e) => cb0(e).filter((e) => cb(e.e)); + // return this; + } + + static fromEvent(element, eventName) { + const o = new MyObservable((e) => [e]); + element.addEventListener(eventName, (e) => o.cb(e)); + return o; + } +} + +// run it +const div = document.querySelector('#_1'), + obs = MyObservable.fromEvent(div, 'click'); + +obs + .subscribe((e) => { + pr('e1', e); + }) + .subscribe((e) => { + pr('e2', e); + }) + .map((e) => [e.clientX, e.clientY]) + .filter(([x, y]) => x != y) + .take(3) + .subscribe((e) => { + pr('e3', e); + }); + +div.click(); + +// run book sample +MyObservable.fromEvent(document, 'click') + .filter((c) => c.clientX > window.innerWidth / 2) + .take(5) + .subscribe((c) => console.log('in sample, x:', c.clientX, 'y:', c.clientY)); + +function try_XMLHttpRequest() { + const req = new XMLHttpRequest(); + req.open('get', 'test.css'); + req.onload = () => { + pr('status', req.status); + pr('response', req.response); + }; + req.send(); + pr('req', req); + + window.req = req; +} +// try_XMLHttpRequest(); + +from(['Adrià', 'Julian', 'Jen', 'Sergi']).subscribe( + (x) => console.log(`Next: ${x}`), + (err) => console.log('Error:', err), + () => console.log('Completed'), +); + +of(...['Adrià', 'Julian', 'Jen', 'Sergi']).subscribe( + (x) => console.log(`Next: ${x}`), + (err) => console.log('Error:', err), + () => console.log('Completed'), +); diff --git a/rxjs/html/test/pos.html b/rxjs/html/test/pos.html new file mode 100644 index 0000000000..7da34a7108 --- /dev/null +++ b/rxjs/html/test/pos.html @@ -0,0 +1,37 @@ + + +
+
+
+
+
+ + + diff --git a/rxjs/html/test/test.css b/rxjs/html/test/test.css new file mode 100644 index 0000000000..f6e936e4fa --- /dev/null +++ b/rxjs/html/test/test.css @@ -0,0 +1,8 @@ +.mycontent { + margin: 1em; +} +#_1 { + width: 2em; + height: 2em; + background: darkgrey; +} diff --git a/rxjs/html/test/tests.html b/rxjs/html/test/tests.html new file mode 100644 index 0000000000..835aad6fb0 --- /dev/null +++ b/rxjs/html/test/tests.html @@ -0,0 +1,38 @@ + + + + + Mocha Tests + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + diff --git a/rxjs/package-lock.json b/rxjs/package-lock.json new file mode 100644 index 0000000000..2183f3f685 --- /dev/null +++ b/rxjs/package-lock.json @@ -0,0 +1,176 @@ +{ + "name": "try-rxjs", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "try-rxjs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "chai": "^4.3.6", + "rxjs": "^7.5.6" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "engines": { + "node": "*" + } + }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "engines": { + "node": "*" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "engines": { + "node": "*" + } + }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "engines": { + "node": "*" + } + }, + "node_modules/rxjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", + "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + } + }, + "dependencies": { + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + }, + "chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==" + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "requires": { + "type-detect": "^4.0.0" + } + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==" + }, + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "requires": { + "get-func-name": "^2.0.0" + } + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" + }, + "rxjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", + "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "requires": { + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + } + } +} diff --git a/rxjs/package.json b/rxjs/package.json new file mode 100644 index 0000000000..f680715e73 --- /dev/null +++ b/rxjs/package.json @@ -0,0 +1,24 @@ +{ + "type": "module", + "name": "try-rxjs", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "mocha .", + "twatch": "mocha -w -parallel ." + }, + "repository": { + "type": "git", + "url": "none" + }, + "keywords": [ + "none" + ], + "author": "", + "license": "ISC", + "dependencies": { + "chai": "^4.3.6", + "rxjs": "^7.5.6" + } +} diff --git a/rxjs/smreactjs5-code b/rxjs/smreactjs5-code new file mode 120000 index 0000000000..1ae44d4c24 --- /dev/null +++ b/rxjs/smreactjs5-code @@ -0,0 +1 @@ +../../rxjs/smreactjs5-code/ \ No newline at end of file diff --git a/rxjs/tests/a.cjs b/rxjs/tests/a.cjs new file mode 100644 index 0000000000..624f6a00a0 --- /dev/null +++ b/rxjs/tests/a.cjs @@ -0,0 +1,15 @@ +// let rxjs; + +// if (typeof window === 'undefined') { +// rxjs = await import('rxjs'); + +// } else { +// rxjs = await import('rxjs'); +// } + +let { range } = require('rxjs'); + + +const pr = console.log; + +pr('hi'); diff --git a/rxjs/tests/a.spec.cjs b/rxjs/tests/a.spec.cjs new file mode 100644 index 0000000000..ad39c219d3 --- /dev/null +++ b/rxjs/tests/a.spec.cjs @@ -0,0 +1,64 @@ +// dynamic import +// if (typeof window === 'undefined') { +// rxjs = await import('rxjs'); + +// } else { +// rxjs = await import('rxjs'); +// } + +const pr = console.log; +let { + flatMap, + of, + from, + take, + reduce, + range, + filter, + map, + bindNodeCallback, +} = require('rxjs'); +let { assert } = require('chai'); + +describe('misc', () => { + it('range', () => { + let acc = []; + const r = range(1, 10).pipe( + filter((x) => x % 2 == 0), + map((x) => x * 2), + ); + r.subscribe((x) => acc.push(x)); + assert.deepEqual([4, 8, 12, 16, 20], acc); + + // another sink + let acc2 = []; + r.subscribe((x) => acc2.push(x)); + assert.deepEqual([4, 8, 12, 16, 20], acc2); + + range(1, 5) + .pipe(reduce((acc, x) => acc.concat([x]), [])) + .subscribe((x) => assert.deepEqual([1, 2, 3, 4, 5], x)); + }); + + it('node.fs.readdir', () => { + const fs = require('fs'), + readdir = bindNodeCallback(fs.readdir), + source = readdir('.'), + subscription = source.subscribe( + (res) => pr(`List of .: ${res}`, typeof res, Array.isArray(res)), + (err) => pr(`err: ${err}`), + () => pr('Done'), + ); + }); + + it('flatMap', () => { + from([of(1, 2, 3), from([4, 5, 6])]) + .pipe( + flatMap((x) => x), + reduce((acc, x) => [x, ...acc], []), + ) + .subscribe((x) => { + assert.deepEqual([1, 2, 3, 4, 5, 6].reverse(), x); + }); + }); +}); diff --git a/rxjs/tests/t.js b/rxjs/tests/t.js new file mode 100644 index 0000000000..f9898bf0d4 --- /dev/null +++ b/rxjs/tests/t.js @@ -0,0 +1,9 @@ +import { assert } from 'chai'; +import { range } from 'rxjs'; +const pr = console.log; + +describe('normal module', () => { + it('deepEqual []', () => { + assert.deepEqual([1, 2], [1, 2]); + }); +}); diff --git a/tictac/game.html b/tictac/game.html new file mode 100644 index 0000000000..e765a55682 --- /dev/null +++ b/tictac/game.html @@ -0,0 +1,33 @@ + + + + + Tik-Tac + + + +

Tic-Tac

+
+ Status: + _ +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + diff --git a/tictac/misc.spec.js b/tictac/misc.spec.js new file mode 100644 index 0000000000..a06bb6eb7a --- /dev/null +++ b/tictac/misc.spec.js @@ -0,0 +1,173 @@ +const assert = chai.assert; + +describe('equality', () => { + it('check deepEqual and equal', () => { + assert.notEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); + assert.deepEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); + assert.notDeepEqual({ a: 1, b: 2, c: 4 }, { a: 1, b: 2, c: 0 }); + }); +}); + +describe('tictac', () => { + it('terminal', () => { + let b = ['X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X']; + assert.equal(true, terminal(b)); + b = ['X', 'O', 'X', 'O', 'X', 'X', 'X', 'X', 'X']; + assert.equal(true, terminal(b)); + b = [null, 'O', 'X', 'O', 'X', 'O', 'O', 'O', 'X']; + assert.equal(false, terminal(b)); + + b = Array(9).fill(null); + assert.equal(false, terminal(b)); + }); + + it('winner and utlity', () => { + let b = ['X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X']; + assert.equal('X', winner(b)); + assert.equal(1, utility(b)); + + b = ['O', 'X', 'X', 'X', 'O', 'X', 'X', 'X', 'O']; + assert.equal('O', winner(b)); + assert.equal(-1, utility(b)); + + b = ['O', 'X', 'X', 'O', 'X', null, 'O', 'X', 'O']; + assert.equal('O', winner(b)); + assert.equal(-1, utility(b)); + + b = ['O', 'X', 'X', 'X', 'X', null, 'O', null, 'O']; + assert.equal(null, winner(b)); + assert.equal(0, utility(b)); + }); + + it('max', () => { + assert.deepEqual([1, 9], max([[1, 9]])); + assert.deepEqual( + [1, 9], + max([ + [1, 9], + [0, 8], + ]), + ); + assert.deepEqual( + [3, 9], + max([ + [2, 0], + [3, 9], + [3, 1], + [1, 8], + ]), + ); + }); + + it('min', () => { + assert.deepEqual([1, 9], min([[1, 9]])); + assert.deepEqual( + [0, 8], + min([ + [1, 9], + [0, 8], + ]), + ); + assert.deepEqual( + [0, 9], + min([ + [2, 0], + [3, 9], + [0, 9], + [1, 8], + ]), + ); + }); +}); + +// int -> [int] +function* primes() { + const res = [2]; + yield 2; + for (let i = 3; ; i += 2) { + if (!res.some((x) => i % x == 0)) { + res.push(i); + yield i; + } + } +} + +function primesN(n) { + let res = []; + for (const x of primes()) { + if (x < n) { + res.push(x); + } else break; + } + return res; +} + +function primesMiddle3() { + let res = new Set(); + for (const x of primes()) { + if (x > 1000) return res; + if (x > 100) { + res.add(parseInt(String(x)[1])); + } + } + return res; +} + +describe('primes', () => { + it('primesN', () => { + let res = primesN(22); + assert.deepEqual([2, 3, 5, 7, 11, 13, 17, 19], res); + }); + it('primesMiddle', () => { + let res = primesMiddle3(), + a = new Array(10).fill(null).map((x, i) => i), + exp = new Set(a); + + assert.deepEqual(exp, res); + }); +}); + +// Jonson-Trotter algo +// int -> [[int]] +function permJ(n) { + if (n == 1) return [[1]]; + return permJ(n - 1) + .map((p) => insert(p, n)) + .flat(1); +} + +// [int]-> [[int]] +// insert x into xs on every index from 0 to len(xs) +function insert(xs, x) { + const res = []; + for (let i = 0; i < xs.length + 1; i++) { + res.push(xs.slice(0, i).concat([x], xs.slice(i))); + } + return res; +} + +describe('perm', () => { + it('perm Jonson-Trotter', () => { + assert.deepEqual([[1]], permJ(1)); + + assert.deepEqual( + new Set([ + [1, 2], + [2, 1], + ]), + new Set(permJ(2)), + ); + + assert.deepEqual( + new Set([ + [1, 2, 3], + [1, 3, 2], + [2, 1, 3], + [2, 3, 1], + [3, 1, 2], + [3, 2, 1], + ]), + new Set(permJ(3)), + ); + }); +}); diff --git a/tictac/tests.html b/tictac/tests.html new file mode 100644 index 0000000000..d706c35861 --- /dev/null +++ b/tictac/tests.html @@ -0,0 +1,32 @@ + + + + + Mocha Tests + + + + +
+ + + + + + + + + + + + + diff --git a/tictac/tictac.css b/tictac/tictac.css new file mode 100644 index 0000000000..216350d810 --- /dev/null +++ b/tictac/tictac.css @@ -0,0 +1,18 @@ +.board { + width: 200px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; + margin-top: 20px; + margin-bottom: 20px; +} + +.cell { + border: 1px solid black; + border-radius: 3px; + height: 1em; + display: flex; + justify-content: center; + align-items: center; + padding: 10px; +} diff --git a/tictac/tictac.js b/tictac/tictac.js new file mode 100644 index 0000000000..2167e885f0 --- /dev/null +++ b/tictac/tictac.js @@ -0,0 +1,300 @@ +const pr = console.log; + +// current state +const world = { board: null, player: null }; + +function newGame() { + // init cell + document.querySelectorAll('.board .cell').forEach((c, i) => { + c.setAttribute('id', i); + c.onclick = (e) => onclick(i); + c.textContent = ''; + }); + + world.board = Array(9).fill(null); + world.player = 'O'; + showStatus(world); +} + +function onclick(i) { + const world1 = makeMove(world, i); + if (world1 && !terminal(world.board)) { + setTimeout(() => aiMove(world1), 300); + } +} + +function makeMove(world, i) { + if (world.board[i] != null) return null; + if (terminal(world.board)) return null; + world.board[i] = world.player; + markCell(i, world.player); + + // pr('b', board); + world.player = nextPlayer(world.player); + showStatus(world); + return world; +} + +// from world state let 2 ai agents play against each other +function aiVsAi(world) { + world = aiMove(world); + if (!world) return null; + setTimeout(() => aiVsAi(world), 150); +} + +function markCell(i, player) { + document.getElementById(String(i)).textContent = player; +} + +function showStatus({ board, player }) { + let s = `player ${player}`, + won = winner(board); + if (won) { + s += `, won ${won}`; + } + if (terminal(board)) { + s += ', game over'; + } + + const e = document.getElementById('status'); + e.textContent = s; +} + +// b -> 1 if X won, -1 if O won, 0 otherwise +function utility(b) { + switch (winner(b)) { + case 'X': + return 1; + case 'O': + return -1; + default: + return 0; + } +} + +function winner(b) { + const won = (ids, p) => ids.every((x) => b[x] === p); + + for (const ids of [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ]) { + if (won(ids, 'X')) return 'X'; + if (won(ids, 'O')) return 'O'; + } + return null; +} + +// b -> T if game is over, F otherwise +function terminal(b) { + return winner(b) != null || b.every((x) => x == 'O' || x == 'X'); +} + +// b -> X or O +function nextPlayer(p) { + return p === 'O' ? 'X' : 'O'; +} + +// find an optimum move and play +function aiMove(w) { + // const bestC = bruteForce(w.board, w.player); + const bestC = minMax(w.board, w.player); + pr('bestC', bestC, w.player); + return makeMove(w, bestC[1]); + // return randMove(w); +} + +function randMove(w) { + const cs = filterNull(w.board); + const i = randSample(cs); + return makeMove(w, i); +} + +function randSample(xs) { + const i = Math.floor(Math.random() * xs.length); + pr('i', i); + return xs[i]; +} + +function filterNull(b) { + return b.reduce((acc, c, i) => { + if (c == null) { + acc.push(i); + } + return acc; + }, []); +} + +const Cash = new Map(); +const cacheKey = (b, p, i) => JSON.stringify([b, p, i]); + +// board, player, cell_i -> Number (score for move into cell_i) +function score(b, p, i) { + // check cache + const k = cacheKey(b, p, i); + if (Cash.has(k)) return Cash.get(k); + + if (terminal(b)) return utility(b); + + const cs = filterNull(b), + b1 = [...b]; + b1[i] = p; + + const res = cs.reduce((acc, c) => (acc += score(b1, nextPlayer(p), c)), 0); + Cash.set(k, res); + return res; +} + +// search through all possible acts and assign every act a score = number of +// ways to win through this act. +// board, player -> cell with max score +function bruteForce(b, p) { + const ss = filterNull(b).map((c) => [score(b, p, c), c]); + const res = ss.reduce((acc, x) => (acc[0] < x[0] ? x : acc)); + pr('res', res); + return res; +} + +// search for an act with the best winning score - max for X and min for O +// assume that both players choose the best act at every step +// board, player -> (score, action) +// function minMax(b, p) { +// if (terminal(b)) return [utility(b), null]; + +// const key = JSON.stringify(b); +// if (Cash.has(key)) return Cash.get(key); + +// let res = null; +// const acts = filterNull(b); +// if (p === 'X') { +// const scoreActs = acts.map((a) => [minMax(result(b, a, 'X'), 'O'), a]); +// res = max(scoreActs); +// } else { +// const scoreActs = acts.map((a) => [minMax(result(b, a, 'O'), 'X'), a]); +// res = min(scoreActs); +// } +// Cash.set(key, res); +// return res; +// } + +function minMax(b, p) { + if (p === 'X') return maxPlayer(b, Infinity); + else return minPlayer(b, -Infinity); +} + +function cachedPlayer(fn) { + function f(b, breakVal) { + if (terminal(b)) return [utility(b), null]; + + const key = JSON.stringify(b); + if (Cash.has(key)) return Cash.get(key); + + res = fn(b, breakVal); + Cash.set(key, res); + return res; + } + return f; +} + +// best act for 'O' search +// board, Num -> [score, act] +function minPlayer(b, breakVal) { + function fn(b, breakVal) { + let res = [Infinity, null]; + + for (const a of filterNull(b)) { + const [score, _] = maxPlayer(result(b, a, 'O'), res[0]); + if (score < res[0]) { + res = [score, a]; + } + if (score < breakVal) break; + } + return res; + } + return cachedPlayer(fn)(b, breakVal); +} + +function maxPlayer(b, breakVal) { + function fn(b, breakVal) { + let res = [-Infinity, null]; + + for (const a of filterNull(b)) { + const [score, _] = minPlayer(result(b, a, 'X'), res[0]); + if (score > res[0]) { + res = [score, a]; + } + if (score > breakVal) break; + } + return res; + } + return cachedPlayer(fn)(b, breakVal); +} + +// best act for 'O' search +// board, Num -> [score, act] +// function minPlayer(b, breakVal) { +// if (terminal(b)) return [utility(b), null]; + +// const key = JSON.stringify(b); +// if (Cash.has(key)) return Cash.get(key); + +// let res = [Infinity, null]; + +// for (const a of filterNull(b)) { +// const [score, _] = maxPlayer(result(b, a, 'O'), res[0]); +// if (score < res[0]) { +// res = [score, a]; +// } +// if (score < breakVal) break; +// } +// Cash.set(key, res); +// return res; +// } + +// best act for 'X' search +// board, Num -> [score, act] +// function maxPlayer(b, breakVal) { +// if (terminal(b)) return [utility(b), null]; + +// const key = JSON.stringify(b); +// if (Cash.has(key)) return Cash.get(key); + +// let res = [-Infinity, null]; + +// for (const a of filterNull(b)) { +// const [score, _] = minPlayer(result(b, a, 'X'), res[0]); +// if (score > res[0]) { +// res = [score, a]; +// } +// if (score > breakVal) break; +// } +// // pr('X', res, breakVal); +// Cash.set(key, res); +// return res; +// } + +// // board -> [score, act] +// function maxValue(b) { +// const acts = filterNull(b); +// const scoreActs = acts.map((a) => [minValue(result(b, a, 'X')), a]); +// return max(scoreActs); +// } + +// [Pair], (Pair, Pair -> bool) -> Pair +function max(xs, pred = (a, b) => a[0] >= b[0]) { + return xs.reduce((acc, x) => (pred(acc, x) ? acc : x)); +} +const min = (xs) => max(xs, (a, b) => a[0] <= b[0]); + +// board, act, player -> board +function result(b, a, p) { + const b1 = [...b]; + b1[a] = p; + return b1; +}