Here are suggested solutions for the études. Of course, your solutions may well be entirely different, and better.
(nsformulas.core(:require[clojure.browser.repl:asrepl]))(defonceconn(repl/connect"http://localhost:9000/repl"))(enable-console-print!)(defndistance"Calculate distance traveled by an object movingwith a given acceleration for a given amount of time"[acceltime](*acceltimetime))(defnkinetic-energy"Calculate kinetic energy given mass and velocity"[mv](/(*mvv)2.0))(defncentripetal"Calculate centripetal acceleration given velocity and radius"[vr](/(*vv)r))(defnaverage"Calculate average of two numbers"[ab](/(+ab)2.0))(defnvariance"Calculate variance of two numbers"[ab](-(*2(+(*aa)(*bb)))(*(+ab)(+ab))))
(defG6.6784e-11)(defngravitational-force"Calculate gravitational force of two objects ofmass m1 and m2, with centers of gravity at a distance r"[m1m2r](/(*Gm1m2)(*rr)))
(defnmonthly-payment"Calculate monthly payment on a loan of amount p,with annual percentage rate apr, and a given number of years"[papryears](let[r(/(/apr100)12.0)n(*years12)factor(.powjs/Math(+1r)n)](*p(/(*rfactor)(-factor1)))))
(defnradians"Convert degrees to radians"[degrees](*(/(.-PIjs/Math)180)degrees))(defndaylight"Find minutes of daylight given latitude in degrees and day of year.Formula from http://mathforum.org/library/drmath/view/56478.html"[lat-degreesday](let[lat(radianslat-degrees)part1(*0.9671396(.tanjs/Math(*0.00860(-day186))))part2(.cosjs/Math(+0.2163108(*2(.atanjs/Mathpart1))))p(.asinjs/Math(*0.39795part2))numerator(+(.sinjs/Math0.01454)(*(.sinjs/Mathlat)(.sinjs/Mathp)))denominator(*(.cosjs/Mathlat)(.cosjs/Mathp))](*60(-24(*7.63944(.acosjs/Math(/numeratordenominator)))))))
(nsdaylight-js.core(:require[clojure.browser.repl:asrepl]))(enable-console-print!)(defonceconn(repl/connect"http://localhost:9000/repl"))(defnradians"Convert degrees to radians"[degrees](*(/(.-PIjs/Math)180)degrees))(defndaylight"Find minutes of daylight given day of year and latitude in degrees.Formula from http://mathforum.org/library/drmath/view/56478.html"[daylat-degrees](let[lat(radianslat-degrees)part1(*0.9671396(.tanjs/Math(*0.00860(-day186))))part2(.cosjs/Math(+0.2163108(*2(.atanjs/Mathpart1))))p(.asinjs/Math(*0.39795part2))numerator(+(.sinjs/Math0.01454)(*(.sinjs/Mathlat)(.sinjs/Mathp)))denominator(*(.cosjs/Mathlat)(.cosjs/Mathp))](*60(-24(*7.63944(.acosjs/Math(/numeratordenominator)))))))(defnget-float-value"Get the floating point value of a field"[field](.parseFloatjs/window(.-value(.getElementByIdjs/documentfield))))(defncalculate[evt](let[lat-d(get-float-value"latitude")julian(get-float-value"julian")minutes(daylightlat-djulian)](set!(.-innerHTML(.getElementByIdjs/document"result"))minutes)))(.addEventListener(.getElementByIdjs/document"calculate")"click"calculate)
Much of the code is duplicated from the previous étude. Only new code is shown here, with ellipses to represent omitted code:
(nsdaylight-gc.core(:require[clojure.browser.repl:asrepl][goog.dom:asdom][goog.events:asevents]))...(defnradians...)(defndaylight...)(defnget-float-value"Get the floating point value of a field"[field](.parseFloatjs/window(.-value(dom/getElementfield))))(defncalculate[evt](let[lat-d(get-float-value"latitude")julian(get-float-value"julian")minutes(daylightlat-djulian)](dom/setTextContent(dom/getElement"result")minutes)))(events/listen(dom/getElement"calculate")"click"calculate)
Much of the code is duplicated from the previous étude. Only new code is shown here, with ellipses to represent omitted code:
(nsdaylight-dommy.core(:require[clojure.browser.repl:asrepl][dommy.core:asdommy:refer-macros[selsel1]]))...(defnradians...)(defndaylight...)(defnget-float-value"Get the floating point value of a field"[field](.parseFloatjs/window(dommy/value(sel1field))))(defncalculate[evt](let[lat-d(get-float-value"#latitude")julian(get-float-value"#julian")minutes(daylightlat-djulian)](dommy/set-text!(sel1"#result")minutes)))(dommy/listen!(sel1"#calculate"):clickcalculate)
Much of the code is duplicated from the previous étude. Only new code is shown here, with ellipses to represent omitted code:
(nsdaylight-domina.core(:require[clojure.browser.repl:asrepl][domina][domina.events:asevents]))...(defnradians...)(defndaylight...)(defnget-float-value"Get the floating point value of a field"[field](.parseFloatjs/window(domina/value(domina/by-idfield))))(defncalculate[evt](let[lat-d(get-float-value"latitude")julian(get-float-value"julian")minutes(daylightlat-djulian)](domina/set-text!(domina/by-id"result")minutes)))(events/listen!(domina/by-id"calculate"):clickcalculate)
Much of the code is duplicated from the previous étude. Only new code is shown here, with ellipses to represent omitted code:
(nsdaylight-enfocus.core(:require[clojure.browser.repl:asrepl][enfocus.core:asef][enfocus.events:asev]))...(defndaylight...)(defnget-float-value"Get the floating point value of a field"[field](.parseFloatjs/window(ef/fromfield(ef/get-prop:value))))(defncalculate[evt](let[lat-d(get-float-value"#latitude")julian(get-float-value"#julian")minutes(daylightlat-djulian)](ef/at"#result"(ef/content(.toStringminutes)))))(ef/at"#calculate"(ev/listen:clickcalculate))
(defnmove-zeros"Move zeros to end of a list or vector of numbers"[numbers](let[nonzero(filter(fn[x](not=x0))numbers)](concatnonzero(repeat(-(countnumbers)(countnonzero))0))))
(nsdaylight-by-date.core(:require[clojure.browser.repl:asrepl][clojure.string:asstr][domina][domina.events:asevents]))(enable-console-print!)(defonceconn(repl/connect"http://localhost:9000/repl"))(defnradians"Convert degrees to radians"[degrees](*(/(.-PIjs/Math)180)degrees))(defndaylight"Find minutes of daylight given latitude in degrees and day of year.Formula from http://mathforum.org/library/drmath/view/56478.html"[lat-degreesday](let[lat(radianslat-degrees)part1(*0.9671396(.tanjs/Math(*0.00860(-day186))))part2(.cosjs/Math(+0.2163108(*2(.atanjs/Mathpart1))))p(.asinjs/Math(*0.39795part2))numerator(+(.sinjs/Math0.01454)(*(.sinjs/Mathlat)(.sinjs/Mathp)))denominator(*(.cosjs/Mathlat)(.cosjs/Mathp))](*60(-24(*7.63944(.acosjs/Math(/numeratordenominator)))))))(defnget-float-value"Get the floating point value of a field"[field](.parseFloatjs/window(domina/value(domina/by-idfield))))(defnleap-year?"Return true if given year is a leap year; false otherwise"[year](or(and(=0(remyear4))(not=0(remyear100)))(=0(remyear400))))(defnordinal-day"Compute ordinal day given Gregorian day, month, and year"[daymonthyear](let[leap(leap-year?year)feb-days(ifleap2928)days-per-month[031feb-days31303130313130313031]month-ok(and(>month0)(<month13))day-ok(andmonth-ok(>day0)(<=day(+(nthdays-per-monthmonth))))subtotal(reduce +(takemonthdays-per-month))](ifday-ok(+subtotalday)0)))(defnto-julian"Convert Gregorian date to Julian"[](let[greg(domina/value(domina/by-id"gregorian"))parts(str/splitgreg#"[-/]")[ymd](map(fn[x](.parseIntjs/windowx10))parts)](ordinal-daydmy)))(defncalculate[evt](let[lat-d(get-float-value"latitude")julian(to-julian)minutes(daylightlat-djulian)](domina/set-text!(domina/by-id"result")(str(quotminutes60)"h "(.toFixed(remminutes60)2)"m"))))(events/listen!(domina/by-id"calculate"):clickcalculate)
(defnmean"Compute mean of a sequence of numbers"[x](let[n(countx)](/(apply +x)n)))(defnmedian"Compute median of a sequence of numbers"[x](let[n(countx)remainder(drop(-(int(/n2))1)(sortx))](if(odd?n)(secondremainder)(/(+(firstremainder)(secondremainder))2))))(defngetsums"Reducing function for computing sum and sum of squares.The accumulator is a two-vector with the current sum and sum of squares.Could be made clearer with destructuring, but that's not inthis chapter."[accitem](vector(+(firstacc)item)(+(lastacc)(*itemitem))))(defnstdev"Compute standard deviation of a sequence of numbers"[x](let[[sumsumsq](reducegetsums[00]x)n(countx)](.sqrtjs/Math(/(-sumsq(/(*sumsum)n))(-n1)))))
This solution uses the Domina library to interact with the web page. The ns special form needs to be updated to require the correct libraries:
(nsstats.core(:require[clojure.browser.repl:asrepl][clojure.string:asstr][domina:asdom][domina.events:asev]))
This is the additional code for interacting with the web page:
(defncalculate"Event handler"[evt](let[numbers(mapjs/window.parseFloat(str/split(domina/value(ev/targetevt))#"[, ]+"))](domina/set-text!(domina/by-id"mean")(meannumbers))(domina/set-text!(domina/by-id"median")(mediannumbers))(domina/set-text!(domina/by-id"stdev")(stdevnumbers))));; connect event handler(ev/listen!(domina/by-id"numbers"):changecalculate)
(nsteeth.core(:require[clojure.browser.repl:asrepl]))(defonceconn(repl/connect"http://localhost:9000/repl"))(enable-console-print!)(defpocket-depths[[0],[221221],[312323],[313212],[323221],[231211],[313232],[332131],[433233],[311322],[434323],[231322],[121132],[122323],[132133],[0],[323112],[221132],[211112],[332113],[313232],[331233],[122333],[223233],[222434],[343334],[112312],[223213],[342443],[332123],[222233],[323232]])(defnbad-tooth"Accumulator: vector of bad tooth numbersand current index"[[bad-listindex]tooth](if(some(fn[x](>=x4))tooth)(vector(conjbad-listindex)(incindex))(vectorbad-list(incindex))))(defnalert"Display tooth numbers where any of thepocket depths is 4 or greater."[depths](first(reducebad-tooth[[]1]depths)))
(nsmake_teeth.core(:require[clojure.browser.repl:asrepl]))(defonceconn(repl/connect"http://localhost:9000/repl"))(defnone-tooth"Generate one tooth"[presentprobability](if(=present"F")[](let[base-depth(if(<(rand)probability)23)](loop[n6result[]](if(=n0)result(recur(decn)(conjresult(+base-depth(-1(rand-int3))))))))))(defngenerate-list"Take list of teeth, probability, and current vector of vectors.Add pockets for each tooth."[teeth-presentprobabilityresult](if(empty?teeth-present)result(recur(restteeth-present)probability(conjresult(one-tooth(firstteeth-present)probability)))))(defngenerate-pockets"Take list of teeth present and probability of a good tooth,and create a list of pocket depths."[teeth-presentprobability](generate-listteeth-presentprobability[]))
This suggested solution uses the Enfocus library to interact with the web page:
(nsdaylight-summary.core(:require[clojure.browser.repl:asrepl][enfocus.core:asef][enfocus.events:asev]))(defonceconn(repl/connect"http://localhost:9000/repl"))(enable-console-print!)(defnradians"Convert degrees to radians"[degrees](*(/(.-PIjs/Math)180)degrees))(defndaylight"Find minutes of daylight given day of year and latitude in degrees.Formula from http://mathforum.org/library/drmath/view/56478.html"[lat-degreesday](let[lat(radianslat-degrees)part1(*0.9671396(.tanjs/Math(*0.00860(-day186))))part2(.cosjs/Math(+0.2163108(*2(.atanjs/Mathpart1))))p(.asinjs/Math(*0.39795part2))numerator(+(.sinjs/Math0.01454)(*(.sinjs/Mathlat)(.sinjs/Mathp)))denominator(*(.cosjs/Mathlat)(.cosjs/Mathp))](*60(-24(*7.63944(.acosjs/Math(/numeratordenominator)))))))(defnmake-ranges"Return vector of begin-end ordinal dates for a list of days per month"[mlist](reduce(fn[accx](conjacc(+x(lastacc))))[1](restmlist)))(defmonth-ranges"Days per month for non-leap years"(make-ranges'(0312831303130313130313031)))(defnto-hours-minutes"Convert minutes to hours and minutes"[m](str(quotm60)"h "(.toFixed(modm60)0)"m"))(defnget-value"Get the value from a field"[field](ef/fromfield(ef/get-prop:value)))(defnmean"Compute mean of a sequence of numbers"[x](/(apply +x)(countx)))(defnmean-daylight"Get mean daylight for a range of days"[startfinishlatitude](let[f(fn[x](daylightlatitudex))](mean(mapf(rangestartfinish)))))(defngenerate-averages"Generate monthly averages for a given latitude"[latitude](loop[rangesmonth-rangesresult[]](if(<(countranges)2)result(recur(restranges)(conjresult(mean-daylight(firstranges)(secondranges)latitude))))))(defncalculate[evt](let[fromMenu(first(ef/from"input[name='locationType']"(ef/get-prop:checked)))lat-d(iffromMenu(.parseFloatjs/window(get-value"#cityMenu"))(.parseFloatjs/window(get-value"#latitude")))averages(generate-averageslat-d)](doall(map-indexed(fn[nitem](ef/at(str"#m"(incn))(ef/content(to-hours-minutesitem))))averages))))(ef/at"#calculate"(ev/listen:clickcalculate))
(nscondiments.core(:require[cljs.nodejs:asnodejs]))(nodejs/enable-util-print!)(defxml(js/require"node-xml-lite"));; forward reference(declareprocess-child)(defnprocess-children"Process an array of child nodes, given a current food nameand an accumulated result"[[foodresult]children](let[[final-foodfinal-map](reduceprocess-child[foodresult]children)][final-foodfinal-map]))(defnadd-condiment"Add food to the vector of foods that go with this condiment"[resultfoodcondiment](let[food-list(getresultcondiment)new-list(iffood-list(conjfood-listfood)[food])](assocresultcondimentnew-list)))(defnprocess-child"Given a current food and result map, and an item,return the new food name and result map"[[foodresult]item];; The first child of an element is text - either a food name;; or a condiment name, depending on the element name.(let[firstchild(first(.-childsitem))](cond(=(.-nameitem)"display_name")(vectorfirstchildresult)(.test#"cond_._name"(.-nameitem))(vectorfood(add-condimentresultfoodfirstchild))(and(.-childsitem)(.-namefirstchild))(process-children[foodresult](.-childsitem)):else[foodresult])))(defn-main[](let[docmap(.parseFileSyncxml(nth(.-argvjs/process)2))](println(last(process-children[""{}](.-childsdocmap))))))(set!*main-cli-fn*-main)
This is a sample web server that simply echoes back the user’s input. Use this as a guide for the remainder of the étude:
(nsservertest.core(:require-macros[hiccups.core:ashiccups])(:require[cljs.nodejs:asnodejs][hiccups.runtime:ashiccupsrt]))(nodejs/enable-util-print!)(defexpress(nodejs/require"express"))(defngenerate-pageuser-name(ifquery(.-userNamequery)"")](.sendresponse(hiccups/html[:html[:head[:title"Server Example"][:meta{:http-equiv"Content-type":content"text/html":charset"utf-8"}]][:body[:p"Enter your name:"][:form{:action"/":method"get"}[:input{:name"userName":valueuser-name}][:input{:type"submit":value"Send Data"}]][:p(if(anduser-name(not=user-name""))(str"Pleased to meet you, "user-name".")"")]]]))))(defn-main[](let[app(express)](.getapp"/"generate-page!)(.listenapp3000(fn[](println"Server started on port 3000")))))(set!*main-cli-fn*-main)
This is a solution for the condiment matcher web page. It has separated the code for creating the condiment map from the XML page into a separate file to keep the code cleaner:
(nsfoodserver.mapmaker)(defxml(js/require"node-xml-lite"));; forward reference(declareprocess-child)(defnprocess-children"Process an array of child nodes, given a current food nameand an accumulated result"[[foodresult]children](let[[final-foodfinal-map](reduceprocess-child[foodresult]children)][final-foodfinal-map]))(defnadd-condiment"Add food to the vector of foods that go with this condiment"[resultfoodcondiment](let[food-list(getresultcondiment)new-list(iffood-list(conjfood-listfood)[food])](assocresultcondimentnew-list)))(defnprocess-child"Given a current food and result map, and an item,return the new food name and result map"[[foodresult]item];; The first child of an element is text - either a food name;; or a condiment name, depending on the element name.(let[firstchild(first(.-childsitem))](cond(=(.-nameitem)"display_name")(vectorfirstchildresult)(.test#"cond_._name"(.-nameitem))(vectorfood(add-condimentresultfoodfirstchild))(and(.-childsitem)(.-namefirstchild))(process-children[foodresult](.-childsitem)):else[foodresult])))(defnfoodmap[filename](let[docmap(.parseFileSyncxmlfilename)](last(process-children[""{}](.-childsdocmap)))))
Here is the main file:
(nsfoodserver.core(:require-macros[hiccups.core:ashiccups])(:require[cljs.nodejs:asnodejs][hiccups.runtime:ashiccupsrt][foodserver.mapmaker:asmapmaker][clojure.string:asstr]))(nodejs/enable-util-print!)(defexpress(nodejs/require"express"))(deffoodmap(mapmaker/foodmap"food.xml"))(defncase-insensitive[ab](compare(str/upper-casea)(str/upper-caseb)))(defncondiment-menu"Create HTML menu with the given selectionas the 'selected' item"[selection](map(fn[item][:option(if(=itemselection){:valueitem:selected"selected"}{:valueitem})item])(sortcase-insensitive(keysfoodmap))))(defncompatible-foods"Create unordered list of foods compatible with selected condiment"[selection](ifselection(map(fn[item][:liitem])(sortcase-insensitive(foodmapselection)))nil))(defngenerate-pagechosen-condiment(ifquery(.-condimentquery)"")](.sendresponse(hiccups/html[:html[:head[:title"Condiment Matcher"][:meta{:http-equiv"Content-type":content"text/html; charset=utf-8"}]][:body[:h1"Condiment Matcher"][:form{:action"http://localhost:3000":method"get"}[:select{:name"condiment"}[:option{:value""}"Choose a condiment"](condiment-menuchosen-condiment)][:input{:type"submit":value"Find Compatible Foods"}]][:ul(compatible-foodschosen-condiment)][:p"Source data: ";; URL split across two lines for book width[:a{:href(str"http://catalog.data.gov/dataset/""mypyramid-food-raw-data-f9ed6"))}"MyPyramid Food Raw Data"]" from the Food and Nutrition Service of the "" United States Department of Agriculture."]]]))))(defn-main[](let[app(express)](.getapp"/"generate-page!)(.listenapp3000(fn[](println"Server started on port 3000")))))(set!*main-cli-fn*-main)
Here is the code for reading a file line by line:
;; This is a macro, and must be in Clojure.;; Its name and location is the same as;; the cljs file, except with a .clj extension.(nscljs-made-easy.line-seq(:refer-clojure:exclude[with-open]))(defmacrowith-open[bindings&body](assert(=2(countbindings))"Incorrect with-open bindings")`(let~bindings(try(do~@body)(finally(.closeSynccljs-made-easy.line-seq/fs~(bindings0))))))
(nscljs-made-easy.line-seq(:requireclojure.string)(:require-macros[cljs-made-easy.line-seq:refer[with-open]]))(deffs(js/require"fs"))(defn-read-chunk[fd](let[length128b(js/Buffer.length)bytes-read(.readSyncfsfdb0lengthnil)](if(>bytes-read0)(.toStringb"utf8"0bytes-read))))(defnline-seq([fd](line-seqfdnil))([fdline](if-let[chunk(read-chunkfd)](if(re-find#"\n"(strlinechunk))(let[lines(clojure.string/split(strlinechunk)#"\n")](if(=1(countlines))(lazy-catlines(line-seqfd))(lazy-cat(butlastlines)(line-seqfd(lastlines)))))(recurfd(strlinechunk)))(ifline(listline)()))))
This is the code to create the frequency table:
(nsfrequency.core(:require[cljs.nodejs:asnodejs][clojure.string:asstr][cljs-made-easy.line-seq:ascme]))(nodejs/enable-util-print!)(deffilesystem(js/require"fs"));;require nodejs lib;; These keywords are the "column headers" from the spreadsheet.;; An entry of nil means that I am ignoring that column.(defheaders[:date:timenil:accident:injury:property-damage:fatalnil:vehicle:year:make:model:color:typenil:race:gender:driver-statenil])(defnzipmap-omit-nil"Does the same as zipmap, except when there's a nil in thefirst vector, it doesn't put anything into the map.I wrote it this way just to prove to myself that I could do it.It's easier to just say (dissoc (zipmap a-vec b-vec) nil)"[a-vecb-vec](loop[result{}aa-vecbb-vec](if(or(empty?a)(empty?b))result(recur(if-not(nil?(firsta))(assocresult(firsta)(firstb))result)(resta)(restb)))))(defnadd-row"Convenience function that adds a row from the CSV fileto the data map."[line](zipmap-omit-nilheaders(str/splitline#"\t")))(defncreate-data-structure"Create a vector of maps from a tab-separated value fileand a list of header keywords."[filenameheaders](cme/with-open[file-descriptor(.openSyncfilesystemfilename"r")](reduce(fn[resultline](conjresult(add-rowline)))[](rest(cme/line-seqfile-descriptor)))))(deftraffic(create-data-structure"traffic_july_2014_edited.csv"headers))(defnfrequency-table"Accumulate frequencies for specifier (a heading keywordor a function that returns a value) in data-map,optionally returning a total."[data-mapspecifier](let[result-map(reduce(fn[accitem](let[v(ifspecifier(specifieritem)nil)](assocaccv(+1(getaccv))))){}data-map)result-seq(sort(seqresult-map))freq(map lastresult-seq)][(vec(map firstresult-seq))(vecfreq)(reduce +freq)]))(defn-main[](println"Hello world!"))(set!*main-cli-fn*-main)
The code for reading the CSV file is unchanged from the previous étude, so I won’t repeat it here:
(nscrosstab.core(:require[cljs.nodejs:asnodejs][clojure.string:asstr][cljs-made-easy.line-seq:ascme]))(nodejs/enable-util-print!)(deffilesystem(js/require"fs"));;require nodejs lib;; These keywords are the "column headers" from the spreadsheet.;; An entry of nil means that I am ignoring that column.(defheaders[:date:timenil:accident:injury:property-damage:fatalnil:vehicle:year:make:model:color:typenil:race:gender:driver-statenil])(defnzipmap-omit-nil"Does the same as zipmap, except when there's a nil in thefirst vector, it doesn't put anything into the map.I wrote it this way just to prove to myself that I could do it.It's easier to just say (dissoc (zipmap a-vec b-vec) nil)"[a-vecb-vec](loop[result{}aa-vecbb-vec](if(or(empty?a)(empty?b))result(recur(if-not(nil?(firsta))(assocresult(firsta)(firstb))result)(resta)(restb)))))(defnadd-row"Convenience function that adds a row from the CSV fileto the data map."[line](zipmap-omit-nilheaders(str/splitline#"\t")))(defncreate-data-structure"Create a vector of maps from a tab-separated value fileand a list of header keywords."[filenameheaders](cme/with-open[file-descriptor(.openSyncfilesystemfilename"r")](reduce(fn[resultline](conjresult(add-rowline)))[](rest(cme/line-seqfile-descriptor)))))(deftraffic(create-data-structure"traffic_july_2014_edited.csv"headers))(defnmarginal"Get marginal totals for a frequency map. (Utility function)"[freq](vec(map last(sort(seqfreq)))))(defncross-tab"Accumulate frequencies for given row and column in data-map,returning row and column totals, plus grand total."[data-maprow-speccol-spec]; In the following call to reduce, the accumulator is a; vector of three maps.; The first maps row values => frequency; The second maps column values => frequency; The third is a map of maps, mapping; row values => column values => frequency(let[[row-freqcol-freqcross-freq](reduce(fn[accitem](let[r(ifrow-spec(row-specitem)nil)c(ifcol-spec(col-specitem)nil)][(assoc(firstacc)r(+1(get(firstacc)r)))(assoc(secondacc)c(+1(get(secondacc)c)))(assoc-in(lastacc)[rc](+1(get-in(lastacc)[rc])))]))[{}{}{}]data-map); I need row totals as part of the return, and I also; add them to get grand total - don't want to re-calculaterow-totals(marginalrow-freq)][(vec(sort(keysrow-freq)))(vec(sort(keyscol-freq)))(vec(for[r(sort(keysrow-freq))](vec(for[c(sort(keyscol-freq))](if-let[n(get-incross-freq(listrc))]n0)))))row-totals(marginalcol-freq)(reduce +row-totals)]))(defnfrequency-table"Accumulate frequencies for specifier in data-map,optionally returning a total. Use a call to cross-tabto re-use code."[data-mapspecifier](let[[row-labels_row-totals_grand-total](cross-tabdata-mapspecifiernil)][row-labels(vec(map firstrow-totals))grand-total]))(defn-main[](println"Hello world!"))(set!*main-cli-fn*-main)
The cross-tabulation functions from “Solution 4-4” are moved to a file named crosstab.cljs and the initial (ns...) changed accordingly:
(nstraffic.core(:require-macros[hiccups.core:ashiccups])(:require[cljs.nodejs:asnodejs][clojure.string:asstr][cljs-made-easy.line-seq:ascme][hiccups.runtime:ashiccupsrt][traffic.crosstab:asct]))(nodejs/enable-util-print!)(defexpress(nodejs/require"express"))(deffilesystem(js/require"fs"));;require nodejs lib;; These keywords are the "column headers" from the spreadsheet.;; An entry of nil means that I am ignoring that column.(defheaders[:date:timenil:accident:injury:property-damage:fatalnil:vehicle:year:make:model:color:typenil:race:gender:driver-statenil])(defnzipmap-omit-nil"Does the same as zipmap, except when there's a nil in thefirst vector, it doesn't put anything into the map.I wrote it this way just to prove to myself that I could do it.It's easier to just say (dissoc (zipmap a-vec b-vec) nil)"[a-vecb-vec](loop[result{}aa-vecbb-vec](if(or(empty?a)(empty?b))result(recur(if-not(nil?(firsta))(assocresult(firsta)(firstb))result)(resta)(restb)))))(defnadd-row"Convenience function that adds a row from the CSV fileto the data map."[line](zipmap-omit-nilheaders(str/splitline#"\t")))(defncreate-data-structure"Create a vector of maps from a tab-separated value fileand a list of header keywords."[filenameheaders](cme/with-open[file-descriptor(.openSyncfilesystemfilename"r")](reduce(fn[resultline](conjresult(add-rowline)))[](rest(cme/line-seqfile-descriptor)))))(deftraffic(create-data-structure"traffic_july_2014_edited.csv"headers))(defnday[entry](.substr(:dateentry)32))(defnhour[entry](.substr(:timeentry)02))(deffield-list[["Choose a field"nil]["Day"day]["Hour"hour]["Accident":accident]["Injury":injury]["Property Damage":property-damage]["Fatal":fatal]["Vehicle Year":year]["Vehicle Color":color]["Driver's Race":race]["Driver's Gender":gender]["Driver's State":driver-state]])(defntraffic-menu"Create a <select> menu with the given choice selected"[option-listselection](map-indexed(fn[nitem](let[menu-text(firstitem)][:option(if(=nselection){:valuen:selected"selected"}{:valuen})menu-text]))option-list))(defnfield-name[n](first(getfield-listn)))(defnfield-code[n](last(getfield-listn)))(defnadd-table-row[row-labelcountsrow-totalresult](conjresult(reduce(fn[accitem](conjacc[:tditem]))[:tr[:throw-label]](conjcountsrow-total))))(defnhtml-table[[row-labelscol-labelscountsrow-totalscol-totalsgrand-total]][:div[:table(if(not(nil?(firstcol-labels)))[:thead(reduce(fn[accitem](conjacc[:thitem]))[:tr[:th"\u00a0"]](conjcol-labels"Total"))][:thead[:tr[:th"\u00a0"][:th"Total"]]])(if(not(nil?(firstcol-labels)))(vec(loop[rlrow-labelsfreqcountsrtrow-totalsresult[:tbody]](if-not(empty?rl)(recur(restrl)(restfreq)(restrt)(add-table-row(firstrl)(firstfreq)(firstrt)result))(add-table-row"Total"col-totalsgrand-totalresult))))(vec(loop[rlrow-labelsrtrow-totalsresult[:tbody]](if-not(empty?rl)(recur(restrl)(restrt)(conjresult[:tr[:th(firstrl)][:td(firstrt)]]))(conjresult[:tr[:th"Total"][:tdgrand-total]])))))]])(defnshow-table[row-speccol-spec](cond(and(not=0row-spec)(not=0col-spec))[:div[:h2(str(field-namerow-spec)" vs. "(field-namecol-spec))](html-table(ct/cross-tabtraffic(field-coderow-spec)(field-codecol-spec)))](not=0row-spec)[:div[:h2(field-namerow-spec)](html-table(ct/cross-tabtraffic(field-coderow-spec)nil))]:elsenil))(defngenerate-pagecol-spec(ifquery(js/parseInt(.-columnquery))nil)row-spec(ifquery(js/parseInt(.-rowquery))nil)](.sendresponse(hiccups/html[:html[:head[:title"Traffic Violations"][:meta{:http-equiv"Content-type":content"text/html; charset=utf-8"}][:link{:rel"stylesheet":type"text/css":href"style.css"}]][:body[:h1"Traffic Violations"][:form{:action"http://localhost:3000":method"get"}"Row: "[:select{:name"row"}(traffic-menufield-listrow-spec)]"Column: "[:select{:name"column"}(traffic-menufield-listcol-spec)][:input{:type"submit":value"Calculate"}]](show-tablerow-speccol-spec)[:hr][:p"Source data: "[:a{:href"http://catalog.data.gov/dataset/traffic-violations-56dda"}"Montgomery County Traffic Violation Database"]]]]))))(defn-main[](let[app(express)](.useapp(.staticexpress"."))(.getapp"/"generate-page!)(.listenapp3000(fn[](println"Server started on port 3000")))))(set!*main-cli-fn*-main)
(nsreact_q.core(:require[clojure.browser.repl:asrepl][quiescent.core:asq][quiescent.dom:asd][quiescent.dom.uncontrolled:asdu]))(defonceconn(repl/connect"http://localhost:9000/repl"))(defoncestatus(atom{:w0:h0:proportionaltrue:border-width"3":border-style"none":orig-w0:orig-h0:src"clock.jpg"}))(enable-console-print!)(defonceborder-style-list'("none""solid""dotted""dashed""double""groove""ridge""inset""outset"))(defnresize"Resize the image; if proportional, determine which fieldhas changed and change the other accordingly."[evt](let[{:keys[whproportionalorig-worig-h]}@statustarget(.-targetevt)id(.-idtarget)val(.-valuetarget)](ifproportional(cond(=id"w")(swap!statusassoc:wval:h(int(*(/ valorig-w)orig-h)))(=id"h")(swap!statusassoc:hval:w(int(*(/ valorig-h)orig-w))):else(swap!statusassoc:horig-h:worig-w))(swap!statusassoc(keywordid)(.-valuetarget)))))(defnrecheck"Handle the checkbox. Since the checked property isn't thevalue of the checkbox, I had to set the property by hand"[evt](let[new-checked(not(:proportional@status))](swap!statusassoc:proportionalnew-checked)(set!(.-checked(.-targetevt))new-checked)))(defnchange-border[evt](let[{:keys[border-widthborder-style]}@statustarget(.-targetevt)id(.-idtarget)val(.-valuetarget)](cond(=id"menu")(swap!statusassoc:border-styleval)(=id"bw")(swap!statusassoc:border-widthval))))(defnset-dimensions"Set dimensions of the image once it loads"[evt](let[node(.getElementByIdjs/document"image")id(.-idnode)](swap!statusassoc:orig-w(.-naturalWidthnode):orig-h(.-naturalHeightnode):w(.-naturalWidthnode):h(.-naturalHeightnode))))(q/defcomponentImage"A component that displays an image":name"ImageWidget"[status](d/img{:id"image":src(:srcstatus):width(:wstatus):height(:hstatus):style{:float"right":borderWidth(:border-widthstatus):borderColor"red":borderStyle(:border-stylestatus)}:onLoadset-dimensions}))(q/defcomponentOption[item](d/option{:valueitem}item))(q/defcomponentForm"Input form":name"FormWidget":on-mount(fn[nodeval](set!(.-checked(.getElementByIdjs/document"prop"))(:proportionalval)))[status](d/form{:id"params"}"Width: "(d/input{:type"text":size"5":value(:wstatus):id"w":onChangeresize})"Height: "(d/input{:type"text":size"5":value(:hstatus):id"h":onChangeresize})(d/br)(du/input{:type"checkbox":id"prop":onChangerecheck:value"proportional"})"Preserve Proportions"(d/br)"Border: "(d/input{:type"text":size"5":value(:border-widthstatus):id"bw":onChangechange-border})"px "(applyd/select{:id"menu":onChangechange-border}(mapOptionborder-style-list))))(q/defcomponentInterface"User Interface":name"Interface"[status](d/div{}(Imagestatus)(Formstatus)))(defnrender"Render the current state atom, and schedule a render on the nextframe"[](q/render(Interface@status)(.getElementByIdjs/document"interface"))(.requestAnimationFramejs/windowrender))(render)
(nsreact_r.core(:require[clojure.browser.repl:asrepl][reagent.core:asreagent:refer[atom]]))(defonceconn(repl/connect"http://localhost:9000/repl"))(defoncestatus(atom{:w0:h0:proportionaltrue:border-width"3":border-style"none":orig-w0:orig-h0:src"clock.jpg"}))(enable-console-print!)(defonceborder-style-list'("none""solid""dotted""dashed""double""groove""ridge""inset""outset"))(defnresize"Resize the image; if proportional, determine which fieldhas changed and change the other accordingly."[evt](let[{:keys[whproportionalorig-worig-h]}@statustarget(.-targetevt)id(.-idtarget)val(.-valuetarget)](ifproportional(cond(=id"w")(swap!statusassoc:wval:h(int(*(/ valorig-w)orig-h)))(=id"h")(swap!statusassoc:hval:w(int(*(/ valorig-h)orig-w))):else(swap!statusassoc:horig-h:worig-w))(swap!statusassoc(keywordid)(.-valuetarget)))))(defnrecheck"Handle the checkbox. Since the checked property isn't thevalue of the checkbox, I had to set the property by hand"[evt](let[new-checked(not(:proportional@status))](swap!statusassoc:proportionalnew-checked)(set!(.-checked(.-targetevt))new-checked)))(defnchange-border[evt](let[{:keys[border-widthborder-style]}@statustarget(.-targetevt)id(.-idtarget)val(.-valuetarget)](cond(=id"menu")(swap!statusassoc:border-styleval)(=id"bw")(swap!statusassoc:border-widthval))))(defnset-dimensions"Set dimensions of the image once it loads"[evt](let[node(.getElementByIdjs/document"image")id(.-idnode)](swap!statusassoc:orig-w(.-naturalWidthnode):orig-h(.-naturalHeightnode):w(.-naturalWidthnode):h(.-naturalHeightnode))))(defnimage"A component that displays an image"[][:img{:id"image":src(:src@status):width(:w@status):height(:h@status):style{:float"right":borderWidth(:border-width@status):borderColor"red":borderStyle(:border-style@status)}:on-loadset-dimensions}])(defnoption[item][:option{:valueitem:keyitem}item])(defncbox[](do[:input{:type"checkbox":id"prop":on-changerecheck:value"proportional"}]))(defnform"Input form"[][:form{:id"params"}"Width: "[:input{:type"text":size"5":value(:w@status):id"w":on-changeresize}]"Height: "[:input{:type"text":size"5":value(:h@status):id"h":on-changeresize}][:br](cbox)"Preserve Proportions"[:br]"Border: "[:input{:type"text":size"5":value(:border-width@status):id"bw":on-changechange-border}]"px "[:select{:id"menu":on-changechange-border}(for[itemborder-style-list](optionitem))]])(defninterface-without-init[][:div(image)(form)])(definterface(with-metainterface-without-init{:component-did-mount(fn[this](set!(.-checked(.getElementByIdjs/document"prop"))(:proportional@status)))}))(defnrender"Render the current state atom"[](reagent/render[interface](.getElementByIdjs/document"interface")))(render)
In this étude, I named the project building_usage and had a module named roster.cljs to create the data structures. I also had a module named utils.cljs to handle conversion of time of day to number of minutes past midnight, which makes it easy to calculate durations. There is also a utility routine to convert that format to 24-hour time.
The roster.cljs file includes the raw CSV as a gigantic string (well, if you consider 72K bytes to be gigantic), including columns I am not using. The build-data-structure function creates:
For this very small subset of the data:
(def roster-string "W;01:00 PM;03:25 PM;C283 TH;06:30 PM;09:35 PM;D207 W;02:45 PM;05:35 PM;C244 TH;06:00 PM;09:05 PM;D208")
The resulting map:
{"Wednesday"
{"C" {64 1, 65 1, 66 1, 67 1, 68 1, 69 1, 70 1, 52 1, 53 1, 54 1, 55 1, 56 1,
57 1, 58 1, 59 2, 60 2, 61 2, 62 1, 63 1}},
"Thursday"
{"D" {72 1, 73 1, 74 2, 75 2, 76 2, 77 2, 78 2, 79 2, 80 2, 81 2, 82 2, 83 2,
84 2, 85 1, 86 1}}}
(nsbuilding_usage.roster(:require[clojure.string:asstr][building_usage.utils:asutils]));; many lines omitted(defroster-string"MW;01:00 PM;03:25 PM;C283TH;06:30 PM;09:35 PM;D207W;02:45 PM;05:35 PM;C244TH;06:00 PM;09:05 PM;D208")(defday-map{"M""Monday","T""Tuesday","W""Wednesday","R""Thursday""F""Friday","S""Saturday","N""Sunday"})(defnadd-entries"Increment the usage count for the building on the given days and times.If there is not an entry yet, created 96 zeros (24 hoursat 15-minute intervals)"[accdaybuildingintervals](let[current(get-inacc[(day-mapday)building])before(if(nil?current)(into[](repeat960))current)after(reduce(fn[accitem](assocaccitem(inc(getaccitem))))beforeintervals)](assoc-inacc[(day-mapday)building]after)))(defnbuilding-map-entry"Split incoming line into parts, then add entries into the count vectorfor each day and time interval for the appropriate building."[accline](let[[daysstart-timeend-timeroom](str/splitline#";")day-list(rest(str/split(str/replace(str/replacedays#"TH""R")#"SU""N")#""))start-interval(quot(utils/to-minutesstart-time)15)end-interval(quot(+14(utils/to-minutesend-time))15)building(str/replaceroom#"([A-Z]+).*$""$1")](loop[dday-listresultacc](if(empty?d)result(recur(restd)(add-entriesresult(firstd)building(rangestart-intervalend-interval)))))))(defnbuilding-usage-map[](let[lines(str/split-linesroster-string)](reducebuilding-map-entry{}lines)))(defnroom-list"Create a map building -> set of rooms in building"[accline](let[[___room](str/splitline#";")building(str/replaceroom#"([A-Z]+).*$""$1")current(accbuilding)](assocaccbuilding(if(nil?current)#{room}(conjcurrentroom)))))(defntotal-rooms[]"Create map with building as key and number of rooms in building as value."(let[lines(str/split-linesroster-string)room-list(reduceroom-list{}lines)](into{}(map(fn[[kv]][k(count(room-listk))])room-list))))
(nsbuilding_usage.utils)(defnto-minutes[time-string](let[[_hrminuteam-pm](re-matches#"(?i)(\d\d?):(\d\d)\s*([AP])\.?M\.?"time-string)hour(+(mod(js/parseInthr)12)(if(=(.toUpperCaseam-pm)"A")012))](+(*hour60)(js/parseIntminute))))(defnpad[n](if(<n10)(str"0"n)(.toStringn)))(defnto-am-pm[total-minutes](let[h(quottotal-minutes60)m(modtotal-minutes60)hour(if(=(modh12)0)12(modh12))suffix(if(<h12)"AM""PM")](strhour":"(padm)" "suffix)))(defnto-24-hr[total-minutes](str(pad(quottotal-minutes60))(pad(modtotal-minutes60))))
In this solution, I am using setInterval to advance the animation rather than requestAnimationFrame. This is because I don’t need smooth animation; I really want one “frame” every 1.5 seconds.
(ns^:figwheel-alwaysbuilding_usage.core(:require[building_usage.roster:asroster][building_usage.utils:asutils][goog.dom:asdom][goog.events:asevents]))(enable-console-print!)(defdays["Monday""Tuesday""Wednesday""Thursday""Friday""Saturday""Sunday"])(defbuildings["A""B""C""D""FLD""GYM""M""N""P"])(defsvg(.-contentDocument(dom/getElement"campus_map")));; define your app data so that it doesn't get overwritten on reload(defonceapp-state(atom{:day"Monday":interval24:usage(roster/building-usage-map):room-count(roster/room-count):runningfalse:interval-idnil}))(defnupdate-map[](let[{:keys[dayintervalusageroom-count]}@app-state](doseq[bbuildings](let[n(get-inusage[daybinterval])percent(/n(room-countb))](set!(.-fillOpacity(.-style(.getElementByIdsvg(str"bldg_"b))))percent)(set!(.-textContent(.getElementByIdsvg(str"group_"b)))(str(int(*100(min1.0percent)))"%"))))))(defnupdate-atom[evt](do(swap!app-stateassoc:day(.-value(dom/getElement"day")):interval(quot(utils/to-minutes(.-value(dom/getElement"time")))15))(update-map)))(defndisplay-day-time[dayinterval](set!(.-innerHTML(dom/getElement"show"))(strday" "(utils/to-am-pm(*15interval)))))(declareadvance-time)(defnplay-button[evt](if(@app-state:running)(do(.clearIntervaljs/window(@app-state:interval-id))(swap!app-stateassoc:runningfalse:interval-idnil)(set!(.-value(dom/getElement"time"))(utils/to-am-pm(*15(@app-state:interval))))(set!(.-className(dom/getElement"edit"))"visible")(set!(.-className(dom/getElement"show"))"hidden")(set!(.-src(dom/getElement"play"))"images/play.svg"))(do(swap!app-stateassoc:runningtrue:interval-id(.setIntervaljs/windowadvance-time1500))(display-day-time(@app-state:day)(@app-state:interval))(set!(.-className(dom/getElement"edit"))"hidden")(set!(.-className(dom/getElement"show"))"visible")(set!(.-src(dom/getElement"play"))"images/pause.svg"))))(defnadvance-time[dom-time-stamp](let[{:keys[daylastUpdateinterval]}@app-statenext-interval(incinterval)](if(>=next-interval96)(play-buttonnil)(do(update-map)(swap!app-stateassoc:intervalnext-interval)(display-day-timedaynext-interval)))))(do(events/listen(dom/getElement"time")"change"update-atom)(events/listen(dom/getElement"day")"change"update-atom)(events/listen(dom/getElement"play")"click"play-button))(defnon-js-reload[];; optionally touch your app-state to force rerendering depending on;; your application;; (swap! app-state update-in [:__figwheel_counter] inc))
<!DOCTYPE html><html><head><linkhref="css/style.css"rel="stylesheet"type="text/css"><metahttp-equiv="Content-Type"content="text/html; charset=utf-8"/></head><body><divid="app"><h2>Building Usage</h2><pclass="bigLabel"><spanid="edit"class="visible"><selectid="day"class="bigLabel"><optionvalue="Monday">Monday</option><optionvalue="Tuesday">Tuesday</option><optionvalue="Wednesday">Wednesday</option><optionvalue="Thursday">Thursday</option><optionvalue="Friday">Friday</option><optionvalue="Saturday">Saturday</option><optionvalue="Sunday">Sunday</option></select><inputclass="bigLabel"id="time"value="6:00 AM"size="8"/></span><spanid="show"class="hidden"></span><imgsrc="images/play.svg"width="45"height="45"alt="play"id="play"/></p><div><objectid="campus_map"data="images/campus_map.svg"type="image/svg+xml"style="border: 1px solid gray"><p>Alas, your browser can not load this SVG file.</p></object></div><scriptsrc="js/compiled/building_usage.js"type="text/javascript"></script></body></html>
(ns^:figwheel-alwaysbuilding_usage2.core(:require[building_usage2.roster:asroster][building_usage2.utils:asutils][goog.dom:asdom][goog.events:asevents]))(enable-console-print!)(defdays["Monday""Tuesday""Wednesday""Thursday""Friday""Saturday""Sunday"])(defbuildings["A""B""C""D""FLD""GYM""M""N""P"])(defbuilding-totals(roster/room-count))(defusage(roster/building-usage-map))(defnmake-labels[items]"Intersperse blank labels between the labels for the hours so thatthe number of labels equals the number of data points."(let[result(reduce(fn[accitem](apply conjacc[item""""""]))[]items)]result))(defncreate-chart[data](let[ctx(.getContext(dom/getElement"chart")"2d")chart(js/Chart.ctx);; Note: everything needs to be converted to JavaScript;; objects and arrays to make Chart.js happy.graph-info#js{:labels(clj->js(make-labels(range024))):datasets#js[#js{:label"Usage":fillColor"rgb(0, 128, 0)":strokeColor"rgb(0, 128, 0)":highlightStroke"rgb(255, 0,0)":data(clj->jsdata)}]};; Override default animation, and set scale;; of y-axis to go from 0-100 in all cases.options#js{:animationfalse:scaleBeginAtZerotrue:scaleShowGridLinestrue:scaleGridLineColor"rgba(0,0,0,.05)":scaleGridLineWidth1:scaleShowVerticalLinestrue:scaleOverridetrue:scaleSteps10:scaleStepWidth10:scaleStartValue0}](.Barchartgraph-infooptions)))(defnto-percent[countsbuilding]"Convert counts of rooms occupied to a percentage;max out at 100%"(let[total(getbuilding-totalsbuilding)](map(fn[item](min100(*100(/itemtotal))))counts)))(defnupdate-graph[evt](let[day(.-value(dom/getElement"day"))building(.-value(dom/getElement"building"))data(if(and(not=""day)(not=""building))(to-percent(get-inusage[daybuilding])building)nil)](if(not(nil?data))(create-chartdata)nil)))(do(events/listen(dom/getElement"day")"change"update-graph)(events/listen(dom/getElement"building")"change"update-graph))(defnon-js-reload[];; optionally touch your app-state to force rerendering depending on;; your application;; (swap! app-state update-in [:__figwheel_counter] inc))
<!DOCTYPE html><html><head><linkhref="css/style.css"rel="stylesheet"type="text/css"/><scripttype="text/javascript"src="Chart.min.js"></script><metahttp-equiv="Content-Type"content="text/html; charset=utf-8"/></head><body><divid="app"><h2>Building Usage</h2><pclass="bigLabel"><selectid="day"class="bigLabel"><optionvalue="">Choose a day</option><optionvalue="Monday">Monday</option><optionvalue="Tuesday">Tuesday</option><optionvalue="Wednesday">Wednesday</option><optionvalue="Thursday">Thursday</option><optionvalue="Friday">Friday</option><optionvalue="Saturday">Saturday</option><optionvalue="Sunday">Sunday</option></select>Building<selectid="building"class="bigLabel"><optionvalue="">--</option><optionvalue="A">A</option><optionvalue="B">B</option><optionvalue="C">C</option><optionvalue="D">D</option><optionvalue="FLD">FLD</option><optionvalue="GYM">Gym</option><optionvalue="M">M</option><optionvalue="N">N</option><optionvalue="P">P</option></select></p><canvasid="chart"width="600"height="300"></canvas><scriptsrc="js/compiled/building_usage2.js"type="text/javascript"></script></div></body></html>
(ns^:figwheel-alwaysproto.core(:require))(enable-console-print!)(defprotocolSpecialNumber(plus[thisb])(minus[thisb])(mul[thisb])(div[thisb])(canonical[this]))(defngcd[mmnn](let[m(js/Math.absmm)n(js/Math.absnn)](cond(=mn)m(>mn)(recur(-mn)n):else(recurm(-nm)))))(defrecordRational[numdenom]Object(toString[r](str(:numr)"/"(:denomr)))SpecialNumber(canonical[r](let[d(if(>=(:denomr)0)(:denomr)(-(:denomr)))n(if(>=(:denomr)0)(:numr)(-(:numr)))g(if(or(zero?n)(zero?d))1(gcdnd))](if-not(=g0)(Rational.(/ng)(/dg))r)))(plus[thisr2](let[{n1:numd1:denom}this{n2:numd2:denom}r2n(+(*n1d2)(*n2d1))d(*d1d2)](if(=d1d2)(canonical(Rational.(+n1n2)d1))(canonical(Rational.nd)))))(minus[r1r2](plusr1(Rational.(-(:numr2))(:denomr2))))(mul[r1r2](canonical(Rational.(*(:numr1)(:numr2))(*(:denomr1)(:denomr2)))))(div[r1r2](canonical(Rational.(*(:numr1)(:denomr2))(*(:denomr1)(:numr2))))))
(ns^:figwheel-alwaysproto.core)(enable-console-print!)(defprotocolSpecialNumber(plus[thisb])(minus[thisb])(mul[thisb])(div[thisb])(canonical[this]));; code for duration and rational not duplicated(defrecordComplex[reim]Object(toString[c](let[{:keys[reim]}c](str(if(zero?re)""re)(if-not(zero?im); note: the order of the conditions here; is absoutely crucial in order to get the; leading minus sign(str(cond(<im0)"-"(zero?re)"":else"+")(js/Math.absim)"i")))))SpecialNumber(canonical[c]c)(plus[thisother](Complex.(+(:rethis)(:reother))(+(:imthis)(:imother))))(minus[thisother](Complex.(-(:rethis)(:reother))(-(:imthis)(:imother))))(mul[thisother]; better living through destructuring(let[{a:reb:im}this{c:red:im}other](Complex.(-(*ac)(*bd))(+(*bc)(*ad)))))(div[thisother](let[{a:reb:im}this{c:red:im}otherdenom(+(*cc)(*dd))]denom(+(*cc)(*dd))](Complex.(/(+(*ac)(*bd))denom)(/(-(*bc)(*ad))denom)))))
(ns^:figwheel-alwaystest.test-cases(:require-macros[cljs.test:refer[deftestisare]])(:require[cljs.test:ast][proto.core:asp]))(deftestduration1(is(=(p/canonical(p/Duration.384))(p/Duration.424))))(deftestduration-str(are[m1s1expected](=(str(p/Duration.m1s1)expected))110"1 10"19"1 09"160"2 00"3145"5 25"00"0 00"))(deftestgcd-test(are[xy](=xy)(p/gcd35)1(p/gcd1214)2(p/gcd3555)5))(deftestrational-plus(are[xyz](let[[ab]x[cd]y[rnrd]z](=(p/plus(p/Rational.ab)(p/Rational.cd))(p/Rational.rnrd)))[12][13][56][28][312][12][04][05][020][10][10][20]))(deftestrational-minus(are[xyz](let[[ab]x[cd]y[rnrd]z](=(p/minus(p/Rational.ab)(p/Rational.cd))(p/Rational.rnrd)))[68][612][14][14][34][-12][14][14][04]))(deftestrational-multiply(are[xyz](let[[ab]x[cd]y[rnrd]z](=(p/mul(p/Rational.ab)(p/Rational.cd))(p/Rational.rnrd)))[13][14][112][34][43][11]))(deftestrational-divide(are[xyz](let[[ab]x[cd]y[rnrd]z](=(p/div(p/Rational.ab)(p/Rational.cd))(p/Rational.rnrd)))[13][14][43][34][43][916]))(deftestcomplex-str(are[riresult](=(str(p/Complex.ri))result)37"3+7i"3-7"3-7i"-37"-3+7i"-3-7"-3-7i"07"7i"30"3"))(deftestcomplex-math(are[r1i1fr2i2r3i3](=(f(p/Complex.r1i1)(p/Complex.r2i2))(p/Complex.r3i3))12p/plus34461-2p/plus-34-2212p/minus34-2-212p/mul34-51002p/mul3-48634p/div122.2-0.41-2p/div3-40.44-0.08))
(ns^:figwheel-alwaysasync1.core(:require-macros[cljs.core.async.macros:refer[gogo-loop]])(:require[cljs.core.async:refer[<!>!timeoutalts!chanclose!]]))(enable-console-print!)(defnon-js-reload[])(defannie(chan))(defbrian(chan))(defnannie-send[](go(loop[n5](println"Annie:"n"-> Brian")(>!briann)(if(pos?n)(recur(decn))nil))))(defnannie-send[](go(loop[n5](println"Annie:"n"-> Brian")(>!briann)(when(pos?n)(recur(decn))))))(defnannie-receive[](go-loop[](let[reply(<!brian)](println"Annie:"reply"<- Brian")(if(pos?reply)(recur)(close!annie)))))(defnbrian-send[](go-loop[n5](println"Brian:"n"-> Annie")(>!annien)(when(pos?n)(recur(decn)))))(defnbrian-receive[](go-loop[](let[reply(<!annie)](println"Brian:"reply"<- Annie")(if(pos?reply)(recur)(close!brian)))))(defnasync-test[](do(println"Starting...")(annie-send)(annie-receive)(brian-send)(brian-receive)))
(ns^:figwheel-alwaysasync2.core(:require-macros[cljs.core.async.macros:refer[gogo-loop]])(:require[cljs.core.async:asa:refer[<!>!timeoutalts!chanclose!]]))(enable-console-print!)(defnon-js-reload[])(defndecrement![[from-strfrom-chan][to-strto-chan]&[start-value]](go-loop[n(orstart-value(dec(<!from-chan)))](printlnfrom-str":"n"->"to-str)(>!to-chann)(when-let[reply(<!from-chan)](printlnfrom-str":"reply"<-"to-str)(if(pos?reply)(recur(decreply))(do(close!from-chan)(close!to-chan)(println"Finished"))))))(defnasync-test[](let[annie(chan)brian(chan)](decrement!["Annie"annie]["Brian"brian]8)(decrement!["Brian"brian]["Annie"annie])))
This solution is split into two files: core.cljs and utils.cljs.
(ns^:figwheel-alwayscardgame.core(:require-macros[cljs.core.async.macros:refer[gogo-loop]])(:require[cljs.core.async:refer[<!>!timeoutalts!chanclose!put!]][cardgame.utils:asutils]))(enable-console-print!)(defmax-rounds50);; max # of rounds per game;; create a channel for each player and the dealer(defplayer1(chan))(defplayer2(chan))(defdealer(chan))(defnon-js-reload[]);; I have added a player-name for debug output;;; it's not needed for the program to work.(defnplayer-process"Arguments are channel, channel name, and initialset of cards. Players either give the dealer cardsor receive cards from her. They send their playernumber back to the dealer so that she can distinguishthe inputs. The :show command is for debugging;the :card-count is for stopping a game after agiven number of rounds, and the :quit command finishes the loop."[playerplayer-nameinit-cards](do(println"Starting"player-name"with"init-cards)(go(loop[my-cardsinit-cards](let[[messageargs](<!player)](condp=message:give(do(printlnplayer-name"has"my-cards"sending dealer"(takeargsmy-cards))(>!dealer[player-name(takeargsmy-cards)])(recur(vec(dropargsmy-cards)))):receive(do(printlnplayer-name"receives"args"add to"my-cards)(>!dealer"Received cards")(recur(apply conjmy-cardsargs))):show(do(printlnmy-cards)(recurmy-cards)):card-count(do(>!dealer[player-name(countmy-cards)])(recurmy-cards)):quitnil))))))(defndetermine-game-winner"If either of the players is out of cards, the other player wins."[card1card2](cond(empty?card1)"Player 1"(empty?card2)"Player 2":elsenil))(defnmake-new-pile"Convenience function to join the current pileplus the players' cards into a new pile."[pilecard1card2](apply conj(apply conjpilecard1)card2))(defnput-all!"Convenience function to send same message toall players. The (doall) is necessary to forceevaluation."[info](doall(map(fn[p](put!pinfo))[player1player2])))(defnarrange"Since we can't guarantee which order the cards come in,we arrange the dealer's messages so that player 1's card(s)always precede player 2's card(s)."[[paca][pbcb]](if(=pa"Player 1")[cacb][cbca]))(defndo-battle"Returns a vector giving the winner (if any) and thenew pile of cards, given the current pile, the players' cards,and the number of rounds played.If someone's card is empty, the other person is the winner.If the number of rounds is at the maximum, the person withthe smaller number of cards wins.If one player has a higher card, the other player hasto take all the cards (returning an empty pile); if theymatch, the result is the pile plus the cards"[pilecard1card2n-rounds](let[c1(utils/value(lastcard1))c2(utils/value(lastcard2))game-winner(determine-game-winnercard1card2)new-pile(make-new-pilepilecard1card2)](println(utils/text(lastcard1))"vs."(utils/text(lastcard2)))(when-notgame-winner(cond(>c1c2)(put!player2[:receivenew-pile])(<c1c2)(put!player1[:receivenew-pile])))[game-winner(if(=c1c2)new-pile(vector))]))(defnplay-game"The game starts by dividing the shuffled deck andgives each player half.Pre-battle state: ask each player to give a card(or 3 cards if the pile isn't empty)Battle state: wait for each player to send cards and evalute.Post-battle: wait for person who lost hand (if not a tie)to receive cardsLong-game: too many rounds. Winner is person with most cards"[](let[deck(utils/short-deck)half(/(countdeck)2)](player-processplayer1"Player 1"(vec(takehalfdeck)))(player-processplayer2"Player 2"(vec(drophalfdeck)))(go(loop[pile[]state:pre-battlen-rounds1](condp=state:pre-battle(do(println"** Starting round"n-rounds)(put-all![:give(if(empty?pile)13)])(recurpile:battlen-rounds)):battle(let[d1(<!dealer);; block untild2(<!dealer);; both players send cards[card1card2](arranged1d2)[game-winnernew-pile](do-battlepilecard1card2n-rounds)](<!(timeout300))(if-notgame-winner(recurnew-pile:post-battlen-rounds)(do(put-all![:quitnil])(println"Winner:"game-winner)))):post-battle(do;; wait until player picks up cards(when(empty?pile)(<!dealer))(if(<n-roundsmax-rounds)(recurpile:pre-battle(incn-rounds))(do(put-all![:card-countnil])(recurpile:long-game0)))):long-game(let[[pana](<!dealer)[pbnb](<!dealer)](put-all![:quitnil])(printlnpa"has"na"cards.")(printlnpb"has"nb"cards.")(println"Winner:"(cond(<nanb)pa(>nanb)pb:else"tied"))))))))
(ns^:figwheel-alwayscardgame.utils(:require))(defsuits["clubs""diamonds""hearts""spades"])(defnames["Ace""2""3""4""5""6""7""8""9""10""Jack""Queen""King"]);; If there was no card at all (nil);; return nil, otherwise aces are high.(defnvalue[card](let[v(when-not(nil?card)(modcard13))](if(=v0)13v)))(defntext[card](let[suit(quotcard13)base(modcard13)](if(nil?card)"nil"(str(getnamesbase)" of "(getsuitssuit)))))(defnfull-deck[](shuffle(range052)));; Give a short deck of Ace to 4 in clubs and diamonds only;; for testing purposes.(defnshort-deck[](shuffle(list012345131415161718)))