I have a Map and I want to go through both the values and keys and replace any occurrences of particular objects which meet some set criteria with some other objects, so if I find a key that meets a specific criteria.
I want to swap that key for a different object that still points at the same value object in the map, similarly if I find a value that I want to replace I want the original key to point at the replacement value.
Here is some code that works for a simplified example but it looks quite ugly, is there a nicer way of achieving this, meaning a method which doesn't require that you extract every key and value you want to replace and then write the replacements back in.
It would be nicer to be able to just iterate of the map once rather than iterating over the keys, and then iterating over all the keys and values to be replaced?
void main(){
//replace all values of instance A with an Object and all keys starting with "t" with the same key minus the "t"
var m = {
"one": new A(),
"two": new A(),
"three": new B(),
"four": new B()
};
mapKeyAndValueSwapper(m,
keyMeetsCondition: (k) => k.startsWith("t"),
valueMeetsCondition: (v) => v is A,
keyReplacer: (k) => k.replaceFirst("t", ""),
valueReplacer: (v) => new Object());
print(m);
}
mapKeyAndValueSwapper(Map m, {bool keyMeetsCondition(key), bool valueMeetsCondition(value), dynamic keyReplacer(key), dynamic valueReplacer(value)}){
var keysToReplace = [];
var keysToReplaceValuesFor = [];
var keys = m.keys;
keys.forEach((k){
if(keyMeetsCondition != null){
if(keyMeetsCondition(k)){
keysToReplace.add(k);
}
}
if(valueMeetsCondition != null){
if(valueMeetsCondition(m[k])){
keysToReplaceValuesFor.add(k);
}
}
});
keysToReplaceValuesFor.forEach((k){
m[k] = valueReplacer(m[k]);
});
keysToReplace.forEach((k){
m[keyReplacer(k)] = m.remove(k);
});
}
class A{}
class B{}
I think this does the same:
Map newMap = {};
m.forEach((k, v) {
var key = k;
var value = v;
if(m[k] is A) {
value = new Object();
}
if(k.startsWith('t')) {
key = k.replaceFirst('t', '');
}
newMap[key]=value;
});
You can create an extension on Map that creates a list from the mapEntries and then manipulate that list to update the key (using the index to maintain order), then clear the map and rebuild it from your updated list.
extension MapX<K, V> on Map<K, V> {
bool updateKey({required K currentKey, required K newKey}) {
if (containsKey(currentKey) && !containsKey(newKey)) {
final value = this[currentKey] as V;
final index = keys.toList().indexWhere((k) => k == currentKey);
final mapEntriesList = entries.toList();
mapEntriesList.removeAt(index);
mapEntriesList.insert(index, MapEntry<K,V>(newKey, value));
clear();
addEntries(mapEntriesList);
return true;
} else {
return false;
}
}
}
Related
Beginner here,
I'm a bit lost with es6 set, map and generators.
How can I select an item in a map, then iterate backwards from that point on effectively? Preferably without going through the whole set/map.
let v = myMap.get('key')
so, from 'v' to the beginning of the map (backwards)?
thank you!
You can create a set of iteration helpers and then compound to create the effect you want:
/* iterTo iterates the iterable from the start and up to (inclusive) key is found.
The function "understands" the Map type when comparing keys as well as
any other iterables where the value itself is the key to match. */
function* iterTo(iterable, key) {
for(let i of iterable) {
yield i;
if((iterable instanceof Map && i[0] === key) || i === key)
return;
}
}
// Same as iterTo, but starts at the key and goes all the way to the end
function* iterFrom(iterable, key) {
let found = false;
for(let i of iterable) {
if(found = (found || (iterable instanceof Map && i[0] === key) || i === key))
yield i;
}
}
// reverseIter creates a reverse facade for iterable
function* reverseIter(iterable) {
let all = [...iterable];
for(let i = all.length; i--; )
yield all[i];
}
You can then use and compound like this:
let m = new Map();
m.set(1, 'a');
m.set(2, 'b');
m.set(3, 'c');
m.set(4, 'd');
m.set(5, 'e');
let s = new Set();
s.add(100);
s.add(200);
s.add(300);
s.add(400);
console.log(...iterTo(m, 3), ...iterFrom(m, 3));
console.log(...reverseIter(iterTo(m, 3)), ...reverseIter(iterFrom(m, 3)));
console.log(...reverseIter(iterTo(s, 200)));
What you probably want is a slice of the keys from the first element to the index of the key, reverse that, iterate over that and get the values from the map.
I am assuming your map is an object:
let keys = Object.keys(map);
return keys.slice(0, keys.indexOf('key')).map((k) => map[k]);
You don't really need a generator.
I'm having trouble using Maps in Haxe. I'm trying to create a grid of Tile objects and add them to the Map using their index on the grid as a key. However, when I try to retrieve a Tile from the map using an index I always get a value of null.
Could someone explain why this is happening? I've never used a map before and I don't understand what the issue is. I'm currently using a multidimensional array to get the same functionality, but maps seem more convenient.
private function initTiles():Void
{
var tempTile:Tile;
tileMap = new Map();
for (i in 0...widthTiles)
{
for (j in 0...heightTiles)
{
tempTile = new Tile(i * 32, j * 32);
tileMap.set([i,j],tempTile);
}
}
}
The issue is that the you are not actually creating a multidimensional array, you are creating a single dimensional array where the key type is Array<Int>. If ever in doubt, you can use $type( tileMap ) to get the compiler to tell you what type it thinks you have.
In your case, you would get:
Map<Array<Int>,Tile>; // This is an ObjectMap, where the object is an Array
When what you really want is:
Map<Int, Map<Int,Tile>>; // This is an IntMap, each value holding another IntMap
The reason this is an issue can be seen with this line:
trace( [0,0] == [0,0] ); // False!
Basically, in Haxe equality of objects (including arrays) is based on if they are the exact same object, not if they hold the same values. In this case, you are comparing two different arrays. Even though they hold the same values, they are actually two different objects, and not equal. Therefore they don't make suitable keys for your map.
Here is a working sample for what you need to do:
class Test {
static function main() {
initTiles();
trace( tileMap[3][6] );
}
static var tileMap:Map<Int,Map<Int,Tile>>;
static function initTiles():Void {
var widthTiles = 10;
var heightTiles = 10;
tileMap = new Map();
for (i in 0...widthTiles) {
if ( tileMap[i]==null ) {
// Add the sub-map for this column
tileMap[i] = new Map();
}
for (j in 0...heightTiles) {
// Add the tile for this column & row
tileMap[i][j] = new Tile(i*32, j*32);
}
}
}
}
class Tile {
var x:Int;
var y:Int;
public function new(x:Int, y:Int) {
this.x = x;
this.y = y;
}
}
And to see it in action: http://try.haxe.org/#E14D5 (Open your browser console to see the trace).
I have seen similar questions but I think my scenario is a bit different. Say I define a collection like this:
MyCol = new Meteor.Collection("myCol"
and I want to get a reference to 'MyCol' using the string 'myCol' - I have created the function below which seems to work:
function GetCollectionObject(name) {
for(var key in window) {
var value = window[key];
if (value instanceof Meteor.Collection) {
if (value._name == name) {
return value;
break;
}
}
}
return null;
}
Is this the only/best/most efficient way to do this?
Why don't you store your collections in a dictionary? It's way more efficient.
Dogs = new Meteor.Collection('dogs');
Cats = new Meteor.Collection('cats');
Alpacas = new Meteor.Collection('alpacas');
MyCollections = {
dogs: Dogs,
cats: Cats,
alpacas: Alpacas,
};
...
MyCollections['dogs'].doSomething();
I am searching for a method to intersect my array collections.
I have one collection: allItems and another subSet. I want to create another ArrayCollection where all items which do not exist in subSet will be stored. Is there a way to do this?
working answer provided by eemeli ... here is an alternative implementation optimized for speed (array access instead of calls) and scalability (approach provides O(m+n) instead of O(m*n))...
public static function difference(a:ArrayCollection, b:ArrayCollection):ArrayCollection {
var entry:*, map:Dictionary = new Dictionary(), intersection:Array = [];
for each (entry in a.source) map[entry] = entry;
for each (entry in b.source) delete map[entry];
for each (entry in map) intersection.push(entry);
return new ArrayCollection(intersection);
}
For getting a collection of items not in another you need a set difference algorithm (allItems minus subSet).
public function minus(a:ArrayCollection, b:ArrayCollection):ArrayCollection {
var result:ArrayCollection = new ArrayCollection()
for each (i in a) {
if (!b.contains(i)) {
result.addItem(i)
}
}
return result
}
var allLength:Number = allItems.length;
var intersection:ArrayCollection = new ArrayCollection();
for(var i:Number = 0; i < allLength; i++)
if(subSet.getItemIndex(allItems.getItemAt(i)) == -1)
intersection.addItem(allItems.getItemAt(i));
Note that this will work only if the subset contains the same objects as the super set. If the subset contains different objects with the same property values as of the super set object, you are gonna have to compare their properties separately.
OK, I am sorting an XMLListCollection in alphabetical order. I have one issue though. If the value is "ALL" I want it to be first in the list. In most cases this happens already but values that are numbers are being sorted before "ALL". I want "ALL" to always be the first selection in my dataProvider and then the rest alphabetical.
So I am trying to write my own sort function. Is there a way I can check if one of the values is all, and if not tell it to do the regular compare on the values?
Here is what I have:
function myCompare(a:Object, b:Object, fields:Array = null):int
{
if(String(a).toLowerCase() == 'all')
{
return -1;
}
else
if(String(b).toLowerCase() == 'all')
{
return 1;
}
// NEED to return default comparison results here?
}
//------------------------------
var sort:Sort = new Sort();
sort.compareFunction = myCompare;
Is there a solution for what I am trying to do?
The solution from John Isaacks is awesome, but he forgot about "fields" variable and his example doesn't work for more complicated objects (other than Strings)
Example:
// collection with custom objects. We want to sort them on "value" property
// var item:CustomObject = new CustomObject();
// item.name = 'Test';
// item.value = 'Simple Value';
var collection:ArrayCollection = new ArrayCollection();
var s:Sort = new Sort();
s.fields = [new SortField("value")];
s.compareFunction = myCompare;
collection.sort = s;
collection.refresh();
private function myCompare(a:Object, b:Object, fields:Array = null):int
{
if(String((a as CustomObject).value).toLowerCase() == 'all')
{
return -1;
}
else if(String((b as CustomObject).value).toLowerCase() == 'all')
{
return 1;
}
// NEED to return default comparison results here?
var s:Sort = new Sort();
s.fields = fields;
var f:Function = s.compareFunction;
return f.call(null,a,b,fields);
}
Well I tried something out, and I am really surprised it actually worked, but here is what I did.
The Sort class has a private function called internalCompare. Since it is private you cannot call it. BUT there is a getter function called compareFunction, and if no compare function is defined it returns a reference to the internalCompare function. So what I did was get this reference and then call it.
private function myCompare(a:Object, b:Object, fields:Array = null):int
{
if(String(a).toLowerCase() == 'all')
{
return -1;
}
else if(String(b).toLowerCase() == 'all')
{
return 1;
}
// NEED to return default comparison results here?
var s:Sort = new Sort();
var f:Function = s.compareFunction;
return f.call(null,a,b,fields);
}
Thanks guys, this helped a lot. In our case, we needed all empty rows (in a DataGrid) on the bottom. All non-empty rows should be sorted normally. Our row data is all dynamic Objects (converted from JSON) -- the call to ValidationHelper.hasData() simply checks if the row is empty. For some reason the fields sometimes contain the dataField String value instead of SortFields, hence the check before setting the 'fields' property:
private function compareEmptyAlwaysLast(a:Object, b:Object, fields:Array = null):int {
var result:int;
if (!ValidationHelper.hasData(a)) {
result = 1;
} else if (!ValidationHelper.hasData(b)) {
result = -1;
} else {
if (fields && fields.length > 0 && fields[0] is SortField) {
STATIC_SORT.fields = fields;
}
var f:Function = STATIC_SORT.compareFunction;
result = f.call(null,a,b,fields);
}
return result;
}
I didn't find these approaches to work for my situation, which was to alphabetize a list of Strings and then append a 'Create new...' item at the end of the list.
The way I handled things is a little inelegant, but reliable.
I sorted my ArrayCollection of Strings, called orgNameList, with an alpha sort, like so:
var alphaSort:Sort = new Sort();
alphaSort.fields = [new SortField(null, true)];
orgNameList.sort = alphaSort;
orgNameList.refresh();
Then I copied the elements of the sorted list into a new ArrayCollection, called customerDataList. The result being that the new ArrayCollection of elements are in alphabetical order, but are not under the influence of a Sort object. So, adding a new element will add it to the end of the ArrayCollection. Likewise, adding an item to a particular index in the ArrayCollection will also work as expected.
for each(var org:String in orgNameList)
{
customerDataList.addItem(org);
}
Then I just tacked on the 'Create new...' item, like this:
if(userIsAllowedToCreateNewCustomer)
{
customerDataList.addItem(CREATE_NEW);
customerDataList.refresh();
}