Sorting a threaded conversation - javascript

Not sure how to frame this one, but here goes.
It's a sorting algorithm question
Tools I have to play with are PostgresSQL and python (2.6) on the server and javascript/jquery at the browser end
I need to move data that describes a threaded conversation from a postgres DB to a web page. The data starts off in chronological order, I want to display in threads
Record numbers are small - should never be more than 100 messages returned
So, as a simple example, imagine the table:
ID Reply_To Text
=== ======== =============================
1 0 Hello World
2 0 Make me a sandwich
3 1 Hello back
4 2 No chance
5 1 Who are you?
6 5 Me of course
The end point I want to reach is
Hello World
Hello Back
Who are you?
Me of course
Make me a sandwich
No chance
Or, to put it another way...
ID Reply_To Text
=== ======== =============================
1 0 Hello World
3 1 Hello back
5 1 Who are you?
6 5 Me of course
2 0 Make me a sandwich
4 2 No chance
I'm not after a full solution here, all the ajax, json and formatting stuff I'm happy to get on with.
I'm just having issues getting my head around the neatest way to manage the sort.
SQL? Python? Javascript?
I'm currently playing with array sorts in Javascript (for no better reason than the fact that my python skills are exceptionally weak)
EDIT
At the moment I'm at something like:
function byThread(a,b) {
if (a.reply > b.id && a.reply != 0){
console.log("Compared id=" + a.id + " with id=" + b.id + " and returned -1 ")
return -1;
}
if (a.id > b.reply && b.reply != 0 ){
console.log("Compared id=" + a.id + " with id=" + b.id + " and returned 1 ")
return 1;
}
console.log("Compared id=" + a.id + " with id=" + b.id + " and returned 0 ")
return 0;
}
msg.sort(byThread);
And it's frustratingly close

I've tried to do this in pure sql, because I think that's where the logic should belong. What you need to do is find the list of ids from parent to child, and order by that. Luckily, postgres has array types which can be ordered, so we can use a recursive CTE:
with recursive threaded(id, reply_to, message, order_path) as (
select
parent.id,
parent.reply_to,
parent.message,
NULL::int[] || parent.id -- create an 1-element array with the parent id
from conversation parent
where parent.reply_to is null
UNION ALL
select
reply.id,
reply.reply_to,
reply.message,
t.order_path || reply.id -- append the reply id to the current path
from threaded t
join conversation reply on t.id = reply.reply_to
where reply.reply_to is not null
)
select * from threaded order by order_path;
And the results:
1 NULL "Hello World" "{1}"
3 1 "Hello Back" "{1,3}"
5 1 "Who are you?" "{1,5}"
6 5 "Me of course" "{1,5,6}"
2 NULL "Make me a sandwich" "{2}"
4 2 "No Chance" "{2,4}"
I'm not sure how this will perform though, so you should definitely test and profile this on your real dataset to make sure it's fine. If it's not, perhaps you could look at restructuring your data, and investigating different ways of storing "tree" data in a database. There is a library for django called django-mptt that can efficiently store and retrieve trees. The concept applies to databases in general, but the algorithms for pulling out trees and making sure they stay intact require changes to your application logic, better handled by a library.
Edit:
I should mention that I was originally using just the top-level id for "order_path" as a single number. This answer led me to using an array of ids to guarantee the order all the way down.

You could try something like this on JS side. Since this have replies inside replies, it'd be easy to build a DOM from the convoSorted
var convo = [{id: 1, replyTo: 0, text: "hello world"},
{id: 2, replyTo: 0, text: "Make me a sandwich"},
{id: 3, replyTo: 1, text: "hello back"},
{id: 4, replyTo: 2, text: "no chance"},
{id: 5, replyTo: 1, text: "who are you?"},
{id: 6, replyTo: 5, text: "me of course"},
{id: 7, replyTo: 0, text: "new question"},
{id: 8, replyTo: 7, text: "new answer"}];
var convoSorted = [];
function addToReplies(obj, rply){ //recursive function to find the q. to reply to.
if (obj.id == rply.replyTo){
obj.replies.push({id: rply.id, text: rply.text, replies: []}); //add to the replies array
}
else{
for (k = 0; k < obj.replies.length; k++){
addToReplies(obj.replies[k], rply);
}
}
}
function sortConvo(){
for (i = 0; i < convo.length; i++){
if (convo[i].replyTo == 0){ //if it's not a reply, add to sorted array
convoSorted.push({ id : convo[i].id, text: convo[i].text, replies: [] });
}
else{ //it's a reply, find the question it's replying
for (j = 0; j < convoSorted.length; j++){
addToReplies(convoSorted[j], convo[i]);
}
}
}
}
sortConvo();
console.log(convoSorted);

I feel an idiot - I've been over-complicating this.
All it needed was a bit of lateral thinking (which this discussion has helped with)
msgs=[
{id:1,reply:0,msg:'Hello World'},
{id:2,reply:0,msg:'Make me a sandwich'},
{id:3,reply:1,msg:'Hello back'},
{id:4,reply:2,msg:'No chance'},
{id:5,reply:1,msg:'Who are you?'},
{id:6,reply:5,msg:'Me of course'}
];
msgs.sort(function(a,b){
return a.reply - b.reply;
});
var conversation=$("<div id='threaded'>");
var list = $("<ul>")
.attr("id","thread_" + msgs[0].reply);
var message = $("<li>")
.text(msgs[0].msg)
.attr("id","msg_" + msgs[0].id);
$(list).append(message);
$(conversation).append(list);
for (i=1; i<msgs.length; i++){
var message = $("<li>")
.text(msgs[i].msg)
.attr("id","msg_" + msgs[i].id);
if ($(conversation).find("#msg_" + msgs[i].reply).length == 0){
$(conversation).find("ul:eq(0)").append(message);
} else {
var list = $("<ul>")
.attr("id","thread_" + msgs[i].id);
$(list).append(message);
$(conversation).find("#msg_" + msgs[i].reply).append(list);
}
}
Sometimes I forget just what a powerful tool the dom is

Related

Rebuilding e.namedValues in Index Order, Pushing to Document in Google Apps Script

I have a Google Form that has many different branches; as a result, out of ~100 questions in total, any given response only includes about 20 answers. I'm trying to find a way to remove questions and answers to which the answer is "" (blank) and then push the questions and answers to a Google Document.
I'm using e.namedValues triggered on FormSubmit. But, of course, e.namedValues doesn't actually return the responses in the order that the questions were asked. I need a way to:
Build an array of questions and answers where only those questions and answers are shown where the answer has a response
Sort that array so that the questions and answers are in the order that they were asked
Push that array into a Google Document (ideally, a table, but I'm unable to do that with the arrays that I'm building).
Here's my current code:
function formResponsetoGoogleDoc(e) {
var formID = {{ Insert FormID Here }};
var response = e.namedValues;
var document = DocumentApp.create('Test').getBody();
var questions = FormApp.openById(formID).getItems();
var questionarray = [];
for (Key in response) {
var label = Key;
var data = response[Key];
if (data != "") {
for (q in questions) {
var title = questions[q].getTitle();
if (title == label) {
var number = questions[q].getIndex();
}
}
questionarray.push(number + " - " + label + ": " + data);
document.appendListItem(number + " - " + label + ": " + data)
}
}
questionarray.sort();
Logger.log(questionarray);
}
This code doesn't work. Currently, questionarray gets sorted in the order of questions (e.g.) 0, 0, 15, 16, 17, 18, 18, 19, 2, 20, 21, 23, 24). Also, any time I try to use this as an appendtable, I get "Exception: The parameters (number[]) don't match the method signature for DocumentApp.Body.appendTable."
Would really appreciate any help that you can give. Willing to consider using e.values as well, but I've never figured out how to cull both questions and (non-)answers from the two arrays needed. I have too many questions to justify hardcoding the replacetext options either, so I'm trying to avoid that.
Answer:
You can use filter to remove empty responses while storing the index of the empty value.
Code Example:
Let e.values be:
e.values = ["Response 0", "Response 1", "", "Response 3", "Response 4", "", "", "", "Response 9"];
We can filter out the empty responses and use another array to store the indices which were removed:
function filterValues(e) {
var indices = [];
var filtered = e.values.filter(function (response, i) {
if (response == "") indices.push(i);
return response != "";
});
console.log(filtered);
console.log(indices);
}
Will display the following in the console:
Jan 11, 2021, 10:50:33 PM Debug [ 'Response 0',
'Response 1',
'Response 3',
'Response 4',
'Response 8' ]
Jan 11, 2021, 10:50:33 PM Debug [ 2, 5, 6, 7 ]
References:
Array.prototype.filter() - JavaScript | MDN
You probably need to battle with this problem a while longer because you don't seem to be able to articulate a very specific question. But you seem to have interest in determining which answers have been answered and in what order and also you seem to want the the index of the answers that don't. I believe this data set should provide you some insight as to how you can obtain that information. The information for each form submission is appended to a spreadsheet along with the headers.
function onMySubmit(e) {
const ss=SpreadsheetApp.getActive();
const osh=ss.getSheetByName('Sheet1');
let arr1=[['Header','Value','Index','Column']];
let sh=e.range.getSheet();
let hA=sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0];
let col={};
let hdr={};
hA.forEach((h,i)=>{col[i]=i+1;hdr[i]=h;});
e.values.map((v,i)=>{arr1.push([hdr[i],v,i,col[i]]);});
osh.getRange(osh.getLastRow()+1,1,arr1.length,arr1[0].length).setValues(arr1);
}

Discord.js searching on key terms

I'm trying to create a Discord bot for trading within certain games. So far I have most of the basic commands working--!create creates a trade listing in an SQL database, !find finds one--but it only finds it on the exact same word. What I'm trying to do is make the search less specific so the terms don't have to be exactly equal to show results.
My current code is pretty convoluted and, needless to say, very broken:
var searchTerms = args[1].split(" ");
var output = {};
for (var id in userData) {
for (var offer in userData[id].offers) {
var score = 0;
for (var key in searchTerms) {
if (offer.includes(key)) {
score ++;
}
}
if (score >= searchTerms.length / 2) {
output[id] = userData[id].offers[offer] + " - " + ((score / searchTerms.length) * 100) + "%";
}
}
}
if (output == {}) {
bot.sendMessage({
to: channelID,
message: 'No matching offers found.'
});
} else {
msg = ""
for (id in output) {
msg += '<#' + id + '> - ' + output[id] + " "
}
bot.sendMessage({
to: channelID,
message: Object.keys(output).length + ' offers found: ' + msg
});
}
I'm new to Javascript so I'm not really sure how to get this working. Any tips are appreciated, thanks!
It looks like what you're trying to implement is an mechanism called Fuzzy Search, which user can find similar results using typo or approximate strings.
( Reference: https://en.wikipedia.org/wiki/Approximate_string_matching )
It not really an easy feature for a programming beginner to implement on your own, either the database have to support some kind of fuzzy query, or you'll have to get all the data from database first, and use a JavaScript fuzzy search library to accomplish that.
If you still want to do it, I recommend using Fuse.js, which is able to accomplish fuzzy search in a few lines
//list to be searched
var books = [{
'ISBN': 'A',
'title': "Old Man's War",
'author': 'John Scalzi'
}, {
'ISBN': 'B',
'title': 'The Lock Artist',
'author': 'Steve Hamilton'
}]
// init the search
var options = {
keys: ['title', 'author'],
id: 'ISBN'
}
var fuse = new Fuse(books, options)
fuse.search('old')
// result
[
"A"
]
Fuzzy search is a complex computer science problem, if you want to know more about it and how Fuse.js is implemented, here are a few useful links
An intro to fuzzy string matching
source code of Fuse.js
bitap algorithm (used by fuse.js)

Javascript: sequential, not parallel

I'm working on a project and have trouble to make my javascript work in a sequential way. I know that javascript can execute tasks in parallel so it won't get stuck when you do a request to a server that doesn't respond. This has It's advantages and disadvantages. In my case, It's a pain in the bum.
The scenario is the following, I'm using Firebase to store how many people agree with an opinion. This looks as follows:
I like javascript
- I like it a lot
- I like it
- like nor dislike it
- don't like it
- I basically hate it
People can now select 1 of the options. A record of their selection is then placed under the option. The structure:
-dfd21DeH67 : {
Title : "I like javascript"
options : {
0 : {
option : "I like it a lot"
agrees : {
-dfd21DQH6sq7 : "Jhon"
-dfd21DQH6sq8 : "Luke"
-dfd21DQH6sq9 : "Bruce"
}
}
1 : {
option : "I like it"
agrees : {
-dfd21DQH6sqA : "Harry"
-dfd21DQH6sqB : "Jimmy"
}
}
2 : {
option : "like nor dislike it"
agrees : {
-dfd21DQH6sqC : "Timmy"
-dfd21DQH6sqD : "Paul"
-dfd21DQH6sqE : "Danny"
-dfd21DQH6sqF : "Robin"
-dfd21DQH6sqG : "Dan"
}
}
3 : {
option : "don't like it"
agrees : {
-dfd21DQH6sqH : "Nathan"
-dfd21DQH6sqI : "Jerry"
}
}
4 : {
option : "I basically hate it"
agrees : {
-dfd21DQH6sqJ : "Tony"
}
}
}
}
Now, I want to query it. The results I need for each option are:
- Option (ex. "I like it a lot")
- agree count (ex. n people agreed on this one)
- Percentage (ex. 3 out of 13 said: "I like javascript". this would be ~23%)
Now, getting the "option" and showing this in the browser: success
getting the "agree count": success thanks to numChildren()
getting the "percentage": challenge because I need the sum of them all to calculate this.
Any ideas on how to pull this off? Tried pulling off some callback tricks. Unfortunatly to no avail. Below is my vision:
//javascript
var hash = "-dfd21DeH67";
var fbDB = new Firebase("https://<MyDB>.firebaseio.com/");
var buffer = [];
var sum;
fbDB.child(hash).once("value", function(opinion) {
var thisOpinion = opinion.val();
// First section
$.each(thisOpinion.options, function(i) {
fbPoll.child(hash + "/options/" + i + "/agrees").once("value", function(agrees) {
votes.push(agrees.numChildren());
sum += agrees.numChildren();
});
});
// execute only if the First section is done
// this is currently not the case, this gets executed before
// the first one finishes => result: #ResultsPanel stays empty
$.each(buffer, function(i) {
$("#ResultsPanel").append(
"<div class=\"OptionStatsBlock\">" +
"Option: " + thisOpinion.options[i].option + "<br>" +
"Total: " + buffer[i] + "<br>" +
"Percentage: " + ((buffer[i] / sum) * 100) +
"</div>"
);
});
});
Thanks in advance,
As covered in the comments already: you seem to struggle with the asynchronous nature of Firebase (and most of the modern web). I would encourage you to spend as much time on understanding asynchronous programming as it takes to get rid of the pain it currently causes. It will prevent you far more pain down the line.
One possible solution
Firebase DataSnapshots always contain all data from under the node that you are requesting. So instead of doing a once in a loop (which is almost always a bad idea), you can just request the data one level higher and then loop and use child:
// First section
$.each(thisOpinion.options, function(i) {
var agrees = opinion.child("/options/" + i + "/agrees");
votes.push(agrees.numChildren());
sum += agrees.numChildren();
});
This code executes synchronously.
Note that I didn't test this, so there may be typos. It's basically just a slight modification of your code, that uses DataSnapshot.child: https://www.firebase.com/docs/web/api/datasnapshot/child.html
You can use jQuery Promise to handle this async nature of calls.
http://api.jquery.com/category/deferred-object/
Even this post might help you out -
http://joseoncode.com/2011/09/26/a-walkthrough-jquery-deferred-and-promise/
For one the second $.each is running on an empty buffer array. So it should never even run.
A better approach may be adding an event listener to the "child_added" event similar to below: Referenced from here https://www.firebase.com/docs/web/guide/retrieving-data.html#section-event-types
var ref = new Firebase("https://docs-examples.firebaseio.com/web/saving-data/fireblog/posts");
// Retrieve new posts as they are added to Firebase
ref.on("child_added", function(snapshot) {
var newPost = snapshot.val();
console.log("Author: " + newPost.author);
console.log("Title: " + newPost.title);
});

List array into Javascript array?

Goal:
Pass the following tweet to visualization tool and display tweet on the UI.
Setup:
I'm getting the following 'List' on my UI. I just want to convert this list to javascript array (so that I can pass it to Visualization tools). I have concat'd 'qqq' for end of every field to identify where it ends.
Tweet:
[*high-pitched yelp* http://t.co/qaluND2Lu3qqq, neutralqqq, 0qqq,
I checked in at Starbucks on #Yelp http://t.co/8wRVos8STjqqq, negativeqqq, -0.159316qqq,
i would like to thank yelp for not helping me find any club around santa monica that plays progressive edm / progressive tranceqqq, positiveqqq, 0.372338qqq,
Nice long bar table & upstairs option (# Social Kitchen & Brewery) on #Yelp http://t.co/uhQB003NTiqqq, positiveqqq, 0.567625qqq]
Question:
How do I split by 'qqq' and then put it in a javascript array?
I have tried doing the following:
var str = "*high-pitched yelp* http://t.co/qaluND2Lu3qqq, neutralqqq, 0qqq";
var res = str.split("qqq");
But the method adds more one comma (,) at the end of every qqq. I'm confused.
Can someone help?
Update 2
It might be best to just build a new clean array, without any undefined values.
var str = "*high-pitched yelp* http://t.co/qaluND2Lu3qqq, neutralqqq, 0qqq",
myArray = str.split('qqq'),
newArray = new Array();
for ( i = 0; i < myArray.length; i++ ) { // Assuming array is built starting with the key 0
// Denote any array element that is undefined, or nonsensical white-space
if ( myArray[i] !== "" && myArray[i] !== undefined && myArray[i] !== " " ) {
newArray.push(myArray[i]);
}
}
myArray = newArray;
console.log(myArray);

Javascript array and information retrieve

Ok so I'm not even sure how to title this post but I have a little logical problem that I need help with. So on a front page of a website I need 4 boxes. These four boxes contain 1 image, 1 title, one date. The trick is, these four boxes need to be randomly generated from a list of 10. So in javascript is it possible to create something like an xml structure to pick 4 random from then populate... so the way I want it to work is...
Item 1
date
title
src
Item 2
date
title
src
Item 3
date
title
src
Is it possible to put the items in an array then access the properties of them after they are randomly selected? I could do this with PHP/MySQL but that's very unnecessary for this. any ideas? Thanks!
Short answer, yes. Long answer below.
var obj1 = {
date: "04/12/1989",
title: "My birthday",
src: "path_to_some_image.png"
}
var obj2 = {
date: "12/25/2011",
title: "Christmas",
src: "santa_claus.gif"
}
objs = [obj1, obj2];
rand = Math.floor(Math.random() * objs.length);
console.log(objs[rand].title + " is " + objs[rand].date);
// "My birthday is 04/12/1989"
// or "Christmas is 12/25/2011"
Sure, you can use Math.random for this purpose. Put your list in an Array, then pick a random number between 0 and 9 and choose that item. You'd do this four times and you're good to go... unless you don't want the same item twice (or more often) - which I am pretty sure is what you want. No repetition, I mean. That makes things more interesting, if you don't want a biased probability distribution. In that case, the algorithm goes like this:
i = Random number between 0 and 9 -> pick array[i]
if (i == 9) -> all is well, skip 3
if (i<9) -> swap array[i] and array[9]
j = Random number between 0 and 8 -> pick array[j]
if (j == 8) -> all well, skip 6
if (j < 8) -> swap array[j] and array[8]
k = Random number between 0 and 7 -> pick array[k]
...
You get the pattern.The method is also known as the Fisher-Yates Shuffle.
You should use an array of objects..
var items = [{
'date': 'date of item 1',
'title': 'title of item 1',
'src': 'url/of/image-1'},
{
'date': 'date of item 2',
'title': 'title of item 2',
'src': 'url/of/image-2'},
/* .. more items.. */
{
'date': 'date of item 9',
'title': 'title of item 9',
'src': 'url/of/image-9'},
{
'date': 'date of item 10',
'title': 'title of item 10',
'src': 'url/of/image-10'}];
for (var i = 1; i < 5; i++) {
var item = items.splice(Math.floor(Math.random() * (items.length)), 1)[0];
var el = document.getElementById('item-' + i);
// insert the info you want in the DOM .. i just add it as text..
el.innerHTML= item.date + ' - ' + item.title + ' - ' + item.src;
}
And use a pre-defined html
<div id="item-1"></div>
<div id="item-2"></div>
<div id="item-3"></div>
<div id="item-4"></div>
Demo at http://jsfiddle.net/qkMNb/1/

Categories

Resources