Determine the key of a song by its chords - javascript

How can I programmatically find the key of a song just by knowing the chord sequence of the song?
I asked some people how they would determine the key of a song and they all said they do it 'by ear' or by 'trial and error' and by telling if a chord resolves a song or not... For the average musician that is probably fine, but as a programmer that really isn't the answer that I was looking for.
So I started looking for music related libraries to see if anyone else has written an algorithm for that yet. But although I found a really big library called 'tonal' on GitHub: https://danigb.github.io/tonal/api/index.html I couldn't find a method that would accept an array of chords and return the key.
My language of choice will be JavaScript (NodeJs), but I'm not necessarily looking for a JavaScript answer. Pseudo code or an explanation that can be translated into code without too much trouble would be totally fine.
As some of you mentioned correctly, the key in a song can change. I'm not sure if a change in key could be detected reliably enough. So, for now let's just say, I'm looking for an algorithm that makes a good approximation on the key of a given chord sequence.
...
After looking into the circle of fifths, I think I found a pattern to find all chords that belong to each key. I wrote a function getChordsFromKey(key) for that. And by checking the chords of a chord sequence against every key, I can create an array containing probabilities of how likely it is that the key matches the given chord sequence: calculateKeyProbabilities(chordSequence). And then I added another function estimateKey(chordSequence), which takes the keys with the highest probability-score and then checks if the last chord of the chord sequence is one of them. If that is the case, it returns an array containing only that chord, otherwise it returns an array of all chords with the highest probability-score.
This does an OK job, but it still doesn't find the correct key for a lot of songs or returns multiple keys with equal probabililty. The main problem being chords like A5, Asus2, A+, A°, A7sus4, Am7b5, Aadd9, Adim, C/G etc. that are not in the circle of fifths. And the fact that for instance the key C contains the exact same chords as the key Am, and G the same as Em and so on...
Here is my code:
'use strict'
const normalizeMap = {
"Cb":"B", "Db":"C#", "Eb":"D#", "Fb":"E", "Gb":"F#", "Ab":"G#", "Bb":"A#", "E#":"F", "B#":"C",
"Cbm":"Bm","Dbm":"C#m","Eb":"D#m","Fbm":"Em","Gb":"F#m","Ab":"G#m","Bbm":"A#m","E#m":"Fm","B#m":"Cm"
}
const circleOfFifths = {
majors: ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#','D#','A#','F'],
minors: ['Am','Em','Bm','F#m','C#m','G#m','D#m','A#m','Fm','Cm','Gm','Dm']
}
function estimateKey(chordSequence) {
let keyProbabilities = calculateKeyProbabilities(chordSequence)
let maxProbability = Math.max(...Object.keys(keyProbabilities).map(k=>keyProbabilities[k]))
let mostLikelyKeys = Object.keys(keyProbabilities).filter(k=>keyProbabilities[k]===maxProbability)
let lastChord = chordSequence[chordSequence.length-1]
if (mostLikelyKeys.includes(lastChord))
mostLikelyKeys = [lastChord]
return mostLikelyKeys
}
function calculateKeyProbabilities(chordSequence) {
const usedChords = [ ...new Set(chordSequence) ] // filter out duplicates
let keyProbabilities = []
const keyList = circleOfFifths.majors.concat(circleOfFifths.minors)
keyList.forEach(key=>{
const chords = getChordsFromKey(key)
let matchCount = 0
//usedChords.forEach(usedChord=>{
// if (chords.includes(usedChord))
// matchCount++
//})
chords.forEach(chord=>{
if (usedChords.includes(chord))
matchCount++
})
keyProbabilities[key] = matchCount / usedChords.length
})
return keyProbabilities
}
function getChordsFromKey(key) {
key = normalizeMap[key] || key
const keyPos = circleOfFifths.majors.includes(key) ? circleOfFifths.majors.indexOf(key) : circleOfFifths.minors.indexOf(key)
let chordPositions = [keyPos, keyPos-1, keyPos+1]
// since it's the CIRCLE of fifths we have to remap the positions if they are outside of the array
chordPositions = chordPositions.map(pos=>{
if (pos > 11)
return pos-12
else if (pos < 0)
return pos+12
else
return pos
})
let chords = []
chordPositions.forEach(pos=>{
chords.push(circleOfFifths.majors[pos])
chords.push(circleOfFifths.minors[pos])
})
return chords
}
// TEST
//console.log(getChordsFromKey('C'))
const chordSequence = ['Em','G','D','C','Em','G','D','Am','Em','G','D','C','Am','Bm','C','Am','Bm','C','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Am','Am','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em']
const key = estimateKey(chordSequence)
console.log('Example chord sequence:',JSON.stringify(chordSequence))
console.log('Estimated key:',JSON.stringify(key)) // Output: [ 'Em' ]

The chords in a song of a particular key are predominantly members of the key's scale. I imagine you could get a good approximation statistically (if there is enough data) by comparing the predominant accidentals in the chords listed to the key signatures of the keys.
See https://en.wikipedia.org/wiki/Circle_of_fifths
Of course, a song in any key can/will have accidentals not in the keys scale, so it would likely be a statistical approximation. But over several bars, if you add up the accidentals and filter out all but the ones that occur most often, you may be able to match to a key signature.
Addendum: as Jonas w correctly points out, you may be able to get the signature, but you won't likely be able to determine if it is a major or minor key.

Here's what I came up with. Still new with modern JS so apologies for messiness and bad use of map().
I looked around the internals of the tonal library, it has a function scales.detect(), but it was no good since it required every note present. Instead I used it as inspiration and flattened the progression into a simple note list and checked this in all transpositions as a subset of all the possible scales.
const _ = require('lodash');
const chord = require('tonal-chord');
const note = require('tonal-note');
const pcset = require('tonal-pcset');
const dictionary = require('tonal-dictionary');
const SCALES = require('tonal-scale/scales.json');
const dict = dictionary.dictionary(SCALES, function (str) { return str.split(' '); });
//dict is a dictionary of scales defined as intervals
//notes is a string of tonal notes eg 'c d eb'
//onlyMajorMinor if true restricts to the most common scales as the tonal dict has many rare ones
function keyDetect(dict, notes, onlyMajorMinor) {
//create an array of pairs of chromas (see tonal docs) and scale names
var chromaArray = dict.keys(false).map(function(e) { return [pcset.chroma(dict.get(e)), e]; });
//filter only Major/Minor if requested
if (onlyMajorMinor) { chromaArray = chromaArray.filter(function (e) { return e[1] === 'major' || e[1] === 'harmonic minor'; }); }
//sets is an array of pitch classes transposed into every possibility with equivalent intervals
var sets = pcset.modes(notes, false);
//this block, for each scale, checks if any of 'sets' is a subset of any scale
return chromaArray.reduce(function(acc, keyChroma) {
sets.map(function(set, i) {
if (pcset.isSubset(keyChroma[0], set)) {
//the midi bit is a bit of a hack, i couldnt find how to turn an int from 0-11 into the repective note name. so i used the midi number where 60 is middle c
//since the index corresponds to the transposition from 0-11 where c=0, it gives the tonic note of the key
acc.push(note.pc(note.fromMidi(60+i)) + ' ' + keyChroma[1]);
}
});
return acc;
}, []);
}
const p1 = [ chord.get('m','Bb'), chord.get('m', 'C'), chord.get('M', 'Eb') ];
const p2 = [ chord.get('M','F#'), chord.get('dim', 'B#'), chord.get('M', 'G#') ];
const p3 = [ chord.get('M','C'), chord.get('M','F') ];
const progressions = [ p1, p2, p3 ];
//turn the progression into a flat string of notes seperated by spaces
const notes = progressions.map(function(e) { return _.chain(e).flatten().uniq().value(); });
const possibleKeys = notes.map(function(e) { return keyDetect(dict, e, true); });
console.log(possibleKeys);
//[ [ 'Ab major' ], [ 'Db major' ], [ 'C major', 'F major' ] ]
Some drawbacks:
- doesn't give the enharmonic note you want necessarily. In p2, the more correct response is C# major, but this could be fixed by checking somehow with the original progression.
-‎ won't deal with 'decorations' to chords that are out of the key, which might occur in pop songs, eg. CMaj7 FMaj7 GMaj7 instead of C F G. Not sure how common this is, not too much I think.

Given an array of tones like this:
var tones = ["G","Fis","D"];
We can firstly generate a unique Set of tones:
tones = [...new Set(tones)];
Then we could check for the appearence of # and bs :
var sharps = ["C","G","D","A","E","H","Fis"][["Fis","Cis","Gis","Dis","Ais","Eis"].filter(tone=>tones.includes(tone)).length];
Then do the same with bs and get the result with:
var key = sharps === "C" ? bs:sharps;
However, you still dont know if its major or minor, and many componists do not care of the upper rules (and changed the key inbetween )...

One approach would be to find all the notes being played, and compare to the signature of different scales and see which is the best match.
Normally a scale signature is pretty unique. A natural minor scale will have the same notes as a major scale (that is true for all the modes), but generally when we say minor scale we mean the harmonic minor scale, which has a specific signature.
So comparing what notes are in the chords with your different scales should give you a good estimate. And you could refine by adding some weight to different notes (for example the ones that come up the most, or the first and last chords, the tonic of each chord, etc.)
This seems to handle most basic cases with some accuracy:
'use strict'
const allnotes = [
"C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"
]
// you define the scales you want to validate for, with name and intervals
const scales = [{
name: 'major',
int: [2, 4, 5, 7, 9, 11]
}, {
name: 'minor',
int: [2, 3, 5, 7, 8, 11]
}];
// you define which chord you accept. This is easily extensible,
// only limitation is you need to have a unique regexp, so
// there's not confusion.
const chordsDef = {
major: {
intervals: [4, 7],
reg: /^[A-G]$|[A-G](?=[#b])/
},
minor: {
intervals: [3, 7],
reg: /^[A-G][#b]?[m]/
},
dom7: {
intervals: [4, 7, 10],
reg: /^[A-G][#b]?[7]/
}
}
var notesArray = [];
// just a helper function to handle looping all notes array
function convertIndex(index) {
return index < 12 ? index : index - 12;
}
// here you find the type of chord from your
// chord string, based on each regexp signature
function getNotesFromChords(chordString) {
var curChord, noteIndex;
for (let chord in chordsDef) {
if (chordsDef[chord].reg.test(chordString)) {
var chordType = chordsDef[chord];
break;
}
}
noteIndex = allnotes.indexOf(chordString.match(/^[A-G][#b]?/)[0]);
addNotesFromChord(notesArray, noteIndex, chordType)
}
// then you add the notes from the chord to your array
// this is based on the interval signature of each chord.
// By adding definitions to chordsDef, you can handle as
// many chords as you want, as long as they have a unique regexp signature
function addNotesFromChord(arr, noteIndex, chordType) {
if (notesArray.indexOf(allnotes[convertIndex(noteIndex)]) == -1) {
notesArray.push(allnotes[convertIndex(noteIndex)])
}
chordType.intervals.forEach(function(int) {
if (notesArray.indexOf(allnotes[noteIndex + int]) == -1) {
notesArray.push(allnotes[convertIndex(noteIndex + int)])
}
});
}
// once your array is populated you check each scale
// and match the notes in your array to each,
// giving scores depending on the number of matches.
// This one doesn't penalize for notes in the array that are
// not in the scale, this could maybe improve a bit.
// Also there's no weight, no a note appearing only once
// will have the same weight as a note that is recurrent.
// This could easily be tweaked to get more accuracy.
function compareScalesAndNotes(notesArray) {
var bestGuess = [{
score: 0
}];
allnotes.forEach(function(note, i) {
scales.forEach(function(scale) {
var score = 0;
score += notesArray.indexOf(note) != -1 ? 1 : 0;
scale.int.forEach(function(noteInt) {
// console.log(allnotes[convertIndex(noteInt + i)], scale)
score += notesArray.indexOf(allnotes[convertIndex(noteInt + i)]) != -1 ? 1 : 0;
});
// you always keep the highest score (or scores)
if (bestGuess[0].score < score) {
bestGuess = [{
score: score,
key: note,
type: scale.name
}];
} else if (bestGuess[0].score == score) {
bestGuess.push({
score: score,
key: note,
type: scale.name
})
}
})
})
return bestGuess;
}
document.getElementById('showguess').addEventListener('click', function(e) {
notesArray = [];
var chords = document.getElementById('chodseq').value.replace(/ /g,'').replace(/["']/g,'').split(',');
chords.forEach(function(chord) {
getNotesFromChords(chord)
});
var guesses = compareScalesAndNotes(notesArray);
var alertText = "Probable key is:";
guesses.forEach(function(guess, i) {
alertText += (i > 0 ? " or " : " ") + guess.key + ' ' + guess.type;
});
alert(alertText)
})
<input type="text" id="chodseq" />
<button id="showguess">
Click to guess the key
</button>
For your example, it gives G major, that's because with a harmonic minor scale, there are no D major or Bm chords.
You can try easy ones: C, F, G or Eb, Fm, Gm
Or some with accidents: C, D7, G7 (this one will give you 2 guesses, because there's a real ambiguity, without giving more information, it could be both)
One with accidents but accurate: C, Dm, G, A

You might be able too keep an structure with keys for every "supported" scale, with as value an array with chords matching that scale.
Given a chord progression you can then start by making a shortlist of keys based on your structure.
With multiple matches you can try to make an educated guess. For example, add other "weight" to any scale that matches the root note.

You can use the spiral array, a 3D model for tonality created by Elaine Chew, which has a key detection algorithm.
Chuan, Ching-Hua, and Elaine Chew. "Polyphonic audio key finding using the spiral array CEG algorithm." Multimedia and Expo, 2005. ICME 2005. IEEE International Conference on. IEEE, 2005.
My recent tension model, which is available in a .jar file here, also outputs the key (in addition to the tension measures) based on the spiral array. It can either take a musicXML file or text file as input that just takes a list of pitch names for each 'time window' in your piece.
Herremans D., Chew E.. 2016. Tension ribbons: Quantifying and visualising tonal tension. Second International Conference on Technologies for Music Notation and Representation (TENOR). 2:8-18.

If you're not opposed to switching languages, music21 (my library, disclaimer) in Python would do this:
from music21 import stream, harmony
chordSymbols = ['Cm', 'Dsus2', 'E-/C', 'G7', 'Fm', 'Cm']
s = stream.Stream()
for cs in chordSymbols:
s.append(harmony.ChordSymbol(cs))
s.analyze('key')
Returns: <music21.key.Key of c minor>
The system will know the difference between, say C# major and Db major. It has a full vocabulary of chord names so things like "Dsus2" won't confuse it. The only thing that might bite a newcomer is that flats are written with minus signs so "E-/C" instead of "Eb/C"

There is an online free tool (MazMazika Songs Chord Analyzer), which analyzes and detects the chords of any song very fast. You can process the song through file upload (MP3/WAV) or by pasting YouTube / SoundCloud links. After processing the file, you can play the song while seeing all the chords playing along in-real time, as well as a table containing all the chords, each chord is assigned to a time-position & a number ID, which you can click to go directly to the corresponding chord and it`s time-position.
https://www.mazmazika.com/chordanalyzer

Related

Map.delete() not working on key in parent Map

What I'm doing right now, is deleting any diff that doesn't contain the string, and if the diff's dictionary is empty, then i try to delete the map.
the issue here is that, i can't delete a map with data.delete(map) for some reasons (no errors in console) and any piece of code located after that deletion in the if statement won't run.
here is the code in question:
var data = new Map({"593620 Linked Horizon - Shinzou o Sasageyo! [TV Size]": {"difficulties": {"Titan": 86813}}, "859608 LiSA - ADAMAS (TV Size)": {"difficulties": {"Kibbleru's Blue Rose": 899}},"940746 CHiCO with HoneyWorks - Kimi ga Sora Koso Kanashikere": {"difficulties": {"Taeyang's Extra": 72321}}});
var string = "titan";
Array.from(data.keys()).forEach(function(map) {
if (!(map.toLowerCase().indexOf(string.toLowerCase()) >=0)) {
if (document.getElementById("diff_search_box").checked) {
Array.from(data.get(map).get("difficulties").keys()).forEach(function(diff) {
if (!(diff.toLowerCase().indexOf(string) >= 0)) {
data.get(map).get("difficulties").delete(diff)
}
})
if (Array.from(data.get(map).get("difficulties").keys()).length = 0) {
data.delete(map)
}
}
}
})
in this situation, I'm supposed to get a dictionary such as:
{
"593620 Linked Horizon - Shinzou o Sasageyo! [TV Size]": {
"difficulties": {"Titan": 86813}
}
}
Huge number of problems with this code. My recommendation is don't write so much code without running it to make sure it works first. Write small pieces at a time and run it as you go making sure everything works along the way.
Issue number one is you cannot initialize a map with an object like that. The Map must be initialized with an array of arrays that are each two elements long, each containing the key value pairs for the map. You can fix this by wrapping the object in Object.entries() as that will return the key vale pairs for the object.
Second problem is titan is a string so it should be "titan".
Number three, you're calling .get on an object in the line data.get(map).get("difficulties"). Objects do not have .get, you have to use brackets or dot syntax: data.get(map).difficulties or data.get(map).difficulties.
Fourth, I think you don't actually want to delete the data from the map. If you did, when the user changes the search text the old data would still be gone.
Why are you using map anyways? you can simply use a normal object.
Just do this if you must use maps:
var data = new Map(Object.entries({
"593620 Linked Horizon - Shinzou o Sasageyo! [TV Size]": {
"difficulties": {"Titan": 86813}
},
"859608 LiSA - ADAMAS (TV Size)": {
"difficulties": {"Kibbleru's Blue Rose": 899}
},
"940746 CHiCO with HoneyWorks - Kimi ga Sora Koso Kanashikere": {
"difficulties": {"Taeyang's Extra": 72321}
}
}));
var string = 'titan';
function search(s) {
var r = {};
for( const [key, value] of data ) {
for( const diffKey in value.difficulties ) {
if(diffKey.toLowerCase().indexOf(string) != -1)
r[key] = value;
}
}
return new Map(Object.entries(r));
}
With this function, you can do search(string) and it will return you the map that you were wanting originally.
Mainly you should writing a bunch of code without running anything.
Map needs an iterable like an array passed to it such as:
new Map([['Key 1', 'Value 1'], ['Key 1', 'Value 1']])
You can't pass an object literal to it but you can easily use Object.entries() to extract the needed array from your object.
Then you can use Map.prototype.forEach() to loop over all the Map entries
var data = {"593620 Linked Horizon - Shinzou o Sasageyo! [TV Size]": {"difficulties": {"Titan": 86813}}, "859608 LiSA - ADAMAS (TV Size)": {"difficulties": {"Kibbleru's Blue Rose": 899}},"940746 CHiCO with HoneyWorks - Kimi ga Sora Koso Kanashikere": {"difficulties": {"Taeyang's Extra": 72321}}};
const map = new Map(Object.entries(data));
map.forEach((value, key) =>{
const {difficulties} = value;
console.log('Map key:', key.toLowerCase());
// if(someCondition){
// map.delete(key)
// }
Object.entries(difficulties).forEach(([k,v])=>{
console.log('Diff key:', k, ' Diff value:', v)
// if(k.toLowerCase().includes('titan')){
// delete difficulties[key];
// }
})
console.log('*****************************')
})
surprising to see none of the previous answer saw that, another person on a discord server i do support for software stuff on pointed out the last if condition and the fact it's missing a = so it appear as
if (Array.from(data.get(map).get("difficulties").keys()).length == 0) {
// was = before, == now
data.delete(map)
}
so now i indeed obtain a data dictionary with only 1 element containing the map which also have the difficulty that's contained in the specified string.

ML5: Error: You are passing a target array of shape 11342,1 while using a loss 'categorical_crossentropy'. 'categorical_crossentropy'expects targets

I am attempting to use the ML5 library for classification in a React app that I am building.
I am getting the following error in my browser
Error: You are passing a target array of shape 11342,1 while using a loss 'categorical_crossentropy'. 'categorical_crossentropy'expects targets to be binary matrices (1s and 0s) of shape [samples, classes].
In several Github issues where this error is raised, the explanation is that That error indicates you have just 1 type of objects in your dataset. You must have 2 or more different object classes in your dataset. That is the explanation in the links here and here.
I don't know what this means. I have 6 inputs and 2 outputs in my data. My input will look something like this
let inputs = {
male: 1,
female: 0,
dob: 641710800000,
// have more, but keeping it simple for this example...
}
and my output will look something like this
let output = {
job: 1 // or 0, if they have a job or not, for example. i.e., two possible outputs
}
However, I'm still getting the error. Can someone help me understand why and how to fix it?
Here's my code below:
people_arr = json.voters_arr;
keys = ["male", "female", "dob"];
let model_options = {
inputs: keys,
outputs: ["job"],
task: "classification"
};
let model = ml5.neuralNetwork(model_options);
for (let person of people_arr) {
let inputs = {
male: person.male,
female: person.female,
dob: person.dob
};
let output = {};
output.job = person.job; // either 0 or 1
model.addData(inputs, output);
}
model.normalizeData();
let train_options = { epochs: 100 }
model.train(train_options, whileTraining); // <-- error happening here
.then(() => {
console.log("pre classify");
return model.classify(new_person_arr);
})
.then((err, results) => {
if (err) { console.log("error") }
else {
let new_arr = results.splice(100);
console.log("results : ", new_arr);
setValues({...values, results: new_arr })
}
})
.catch((err) => { console.log("err : ", err) });
Categorical cross-entropy expects a one-hot vector as a label, not a single number. For instance, let's say there are three people: Michael, Jim, and Dwight. Michael and Jim have jobs, Dwight doesn't. Let's say that not having a job puts you in category 0, and having one puts you in category 1. Labels in this case would look like this:
[[0,1], # Michael's label
[0,1], # Jim's label
[1,0]] # Dwight's label
Michael and Jim are in category 1, so they have a 1 at index 1 and a 0 at all other indices. Dwight is in category 0, so he has a 1 at index 0 and a 0 at all other indices.
If you want to use a single number as a label (i.e. either a 0 or a 1), you should use sparse categorical cross-entropy instead. Sparse categorical cross-entropy takes an integer as a label for each sample and assumes that there are categories from 0 to the highest value integer it sees. So it would work perfectly well with what you already have.

Javascript loop works the first time ran, but gives NaN the second time without logic

I'm developing a javascript game and am altering values from a JSON file through a loop. The loop however occasionally replaces the values it should alter with "NaN" and grabs a random letter from the prefix word of the array. I tried debugging the values and putting in fixed creature values, but that has made me none the wiser.
The code works THE FIRST TIME RAN in JSFiddle: https://jsfiddle.net/ezwad5mL/2/ but whenever you run it a second time, it overwrites the values in the loop with NaN and the letter. I think it's because the function random_int needs 2 values but it only inputs 1 the second time you run it, which is somehow the same value from the previous input (which it altered in the second For loop). What I don't understand is how this code doesn't reset the storedDungeon if it fires the second time.
I understand the problem I think, but I have no clue what's wrong with what I wrote and why it does okay the first time, but screws up the second time.
function random_item(items){
return items[Math.floor(Math.random()*items.length)];
}
function random_int(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
var storedDungeon = []
const jsonCreatures = {
"easy": [
{ "name": "Scorchbird", "hp": [6,13], "prefix": ["Weak", "Small", "Young", "Wild"],
"damage": [1,5], "droprateCommon": [0,60], "droprateRare": [70, 90]},
{ "name": "Reanimated Corpse", "hp": [8,15], "prefix": ["Weak", "Festering"], "damage":
[3,5], "droprateCommon": [0,40], "droprateRare": [50, 80]}
]}
var randNumber = 2
for (let i = 0; i < randNumber; i++) {
let randomObject = random_item(jsonCreatures.easy)
storedDungeon.push(randomObject)
}
for (let o = 0; o < storedDungeon.length; o++) {
storedDungeon[o].hp = random_int(storedDungeon[o].hp[0], storedDungeon[o].hp[1])
storedDungeon[o].damage = random_int(storedDungeon[o].damage[0],storedDungeon[o].damage[1])
storedDungeon[o].prefix = random_item(storedDungeon[o].prefix)
}
console.log(storedDungeon)
To understand the problem we need to understand how arrays work.
The following example may open your eyes to the problem.
const creatures = [
{
name: 'Scorchbird'
}
]
const dungeon = []
dungeon.push(creatures[0])
dungeon[0].name = 'Reference to the object!'
console.log(creatures)
// [
// {
// name: 'Reference to the object!'
// }
// ]
When we add a creature (object) to our dungeon array
dungeon.push(creatures[0])
we actually add a reference to the original object, not a copy of it.
This means that when you do
storedDungeon[o].hp = random_int(
storedDungeon[o].hp[0],
storedDungeon[o].hp[1]
)
you alter the original creatures objects and their properties.
In this case the array hp: [6, 13] is replaced with a random (single!) number, for example hp: 8
And when your code runs the second time there is now hp array to do hp[0] and hp[1], just a single number. This is why the function random_int returns NaN (not a number).
The same effect happens with the damage and with the prefix. However because prefix is a string the random_item function will return a random character in this string. That's because the characters of a string can be accessed by their index like in an array: "im a string"[1] = "m"
I think Nico Gräf's explanation is correct. To fix this, you can create a clone of the object and have it pushed to the storedDungeon.
storedDungeon.push({...randomObject})
Please note that spread syntax doesn't deep clone your object. So it only works to the first level of the object, which should be fine with your current design.

How to find the position of all array items from a loop

I'm brand new to programming so I apologize if this is a simple question.
I had a unique practice problem that I'm not quite sure how to solve:
I'm dealing with two arrays, both arrays are pulled from HTML elements on the page, one array is representing a bunch of states, and the next array is representing their populations. The point of the problem is to print the name of the states and their less than average populations.
To find and print all of the populations that are less than the average I used this code:
function code6() {
// clears screen.
clr();
// both variables pull data from HTML elements with functions.
var pop = getData2();
var states = getData();
var sum = 0;
for( var i = 0; i < pop.length; i++ ){
sum += parseInt( pop[i], 10 );
var avg = sum/pop.length;
if (pop[i] < avg) {
println(pop[i]);
// other functions used in the code to get data, print, and clear the screen.
function getData() {
var dataSource = getElement("states");
var numberArray = dataSource.value.split('\n');
// Nothing to split returns ['']
if (numberArray[0].length > 0) {
return(numberArray);
} else {
return [];
}
}
// Get the data from second data column
function getData2() {
var dataSource = getElement("pops");
var numberArray = dataSource.value.split('\n');
// Nothing to split returns ['']
if (numberArray[0].length > 0) {
return(numberArray);
} else {
return [];
}
}
// Clear the 'output' text area
function clr() {
var out = getElement("output");
out.value = "";
}
// Print to the 'output' HTML element and ADDS the line break
function println(x) {
if (arguments.length === 0) x = '';
print(x + '\n');
}
Now I just need to know how to get the value of these positions within the array so I can pull out the same positions from my states array and display them both side by side. Both arrays have the identical amount of items.
I hope this makes sense and thanks in advance to anyone who has time to take a look at this.
Best regards,
-E
Its a little hard to tell what you are trying to accomplish, but I guess you are going for something like:
'use strict'
function code6() {
const populations = ['39000000', '28000000', '21000000'];
const stateNames = ['california', 'texas', 'florida'];
const states = populations.map((population, i) => ({
'name': stateNames[i],
'population': Number(population),
}));
const sum = states.reduce((sum, state) => sum + state.population, 0);
const average = sum / populations.length;
states
.filter(state => state.population < average)
.forEach(state => {
const name = state.name;
const population = state.population;
console.log(`state name: ${name}, population: ${population}`);
});
}
// run the code
code6();
// state name: texas, population: 28000000
// state name: florida, population: 21000000
I took the liberty of refactoring your code to be a little more modern (es6) and Idiomatic. I hope its not to confusing for you. Feel free to ask any questions about it.
In short you should use:
'use strict' at the top of your files
const/let
use map/filter/forEach/reduce to iterate lists.
use meaningfull names
, and you should avoid:
classic indexed for-loop
parseInt
, and pretty much never ever use:
var
If your states array is built with corresponding indices to your pop one, like this:
states; //=> ['Alabama', 'Alaska', 'Arizona', ...]
pop; //=> [4863300, 741894, 6931071, ...]
then you could simply update your print statement to take that into account:
if (pop[i] < avg) {
println(state[i] + ': ' + pop[i]);
}
Or some such.
However, working with shared indices can be a very fragile way to use data. Could you rethink your getData and getData2 functions and combine them into one that returns a structure more like this the following?
states; //=> [
// {name: 'Alabama', pop: 4863300}
// {name: 'Alaska', pop: 741894},
// {name: 'Arizona', pop: 6931071},
// ...]
This would entail changes to the code above to work with the pop property of these objects, but it's probably more robust.
If your pop and state looks like:
var state = ['state1', 'state2', ...];
var pop = ['state1 pop', 'state2 pop', ...];
Then first of all, avg is already wrong. sum's value is running along with the loop turning avg's formula into sum as of iteration / array length instead of sum of all pops / array length. You should calculate the average beforehand. array.reduce will be your friend.
var average = pop.reduce(function(sum, val){return sum + val;}, 0) / pop.length;
Now for your filter operation, you can:
Zip up both arrays to one array using array.map.
Filter the resulting array with array.filter.
Finally, loop through the resulting array using array.forEach
Here's sample code:
var states = ['Alabama', 'Alaska'];
var pop = [4863300, 741894];
var average = pop.reduce(function(sum, val){return sum + val;}) / pop.length;
console.log('Average: ' + average);
states.map(function(state, index) {
// Convert 2 arrays to an array of objects representing state info
return { name: state, population: pop[index] };
}).filter(function(stateInfo) {
console.log(stateInfo);
// Filter each item by returning true on items you want to include
return stateInfo.population < average;
}).forEach(function(stateInfo) {
// Lastly, loop through your results
console.log(stateInfo.name + ' has ' + stateInfo.population + ' people');
});

Dictionary equivalent data structure?

I'm working in JavaScript and want to keep a list of set km/mph approximations to hand. (I can't convert programmatically, I'm working with an external API that expects certain values, so it really does have to be a dictionary equivalent.)
Currently I'm using an object:
var KM_MPH = { 10: 16, 12: 20, 15: 24 };
Going from mph to km is pretty easy:
var km = KM_MPH[10];
How do I find mph, given km? Also, is an object the best data structure to use for this sort of thing in JavaScript? I'm more used to Python.
A basic JavaScript object is in fact the best choice here. To find a reverse mapping, you can do:
function mphToKM(val){
for(var km in KM_MPH){
if(KM_MPH[km] === val){
return km;
}
}
return null;
}
Or, if you anticipate having to do a lot of lookups, I would recommend having a secondary JS Object that is the mirror of the first
var mph_km = {};
for(var km in KM_MPH){
mph_km[KM_MPH[km]] = km;
}
// mph_km[16] ==> 10
I don't know if you are in fact doing this for conversion between kilometres per hour to miles per hour... if so, it seems to make more sense to just do the conversion directly instead of relying on a hash mapping of the values.
var conversionRate = 1.609344; // kilometres per mile
function kphToMPH(val){
return val / conversionRate ;
}
function mphToKPH(val){
return val * conversionRate;
}
You can use iterate over all entries to find to find your key
Mostly a dict is used to from key=>value
Alternatively you can have two lists
var km = [];
var mph = [];
with their corresponding indices mapped
This is much closer to a Dictionary data structure, since you can have dozens of elements:
var dictionary = [
{ key: 10, value: 12 },
{ key: 12, value: 20 },
{ key: 15, value: 24 }
];
Then you can also use some JavaScript Framework like jQuery to filter elements:
var element = $.filter(dictionary, function() {
return $(this).attr("key") == 10;
});
alert($(element).attr("value"));
Yes, the JavaScript object is the correct choice.
Create a second object to do the reverse lookup:
var i, MPH_KM = {};
for(i in KM_MPH) MPH_KM[KM_MPH[i]] = i;
var mph = MPH_KM[16];
The dictionary equivalent structure for a javascript object would look like this:
var dictionary = { keys:[], values:[] };
Above structure is an equivalent of
Dictionary(Of Type, Type) **For VB.Net**
Dictionary<Type, Type>) **For C#.Net**
Hope this helps!

Categories

Resources