Skip to content

Commit

Permalink
Add support for unflatten function (#9)
Browse files Browse the repository at this point in the history
* Add support for unflatten

* chore: Move to separate files

* chore: Configure env

* feat: Basic test

* fix: Mixing flattened maps

* feat: Port flat.js tests

* fix: Remove support for nested maps

* Fixed unflatten function to work with nested objects in arrays, fixed some tests

* fix: Throw ArgumentError if not flat

---------

Co-authored-by: Danilo Fuchs <[email protected]>
  • Loading branch information
Pana-g and danilofuchs authored Nov 7, 2023
1 parent ff3ba1b commit efbb316
Show file tree
Hide file tree
Showing 10 changed files with 534 additions and 224 deletions.
4 changes: 4 additions & 0 deletions .fvm/fvm_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"flutterSdkVersion": "3.13.8",
"flavors": {}
}
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ doc/api/
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map
*.js.map

# FVM config
.fvm/flutter_sdk
.vscode/settings.json

59 changes: 46 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Take a nested Map and flatten it with delimited keys. Entirely based on node.js [flat](https://www.npmjs.com/package/flat).

> It does not achieve feature parity yet, as some options are missing from `flatten` (maxDepth, transformKey) and `unflatten` is not yet implemented.
> It does not achieve feature parity yet, as some options are missing from `flatten` (maxDepth, transformKey).
> Currently, it bails out of a tree if it finds something different than a `Map` or `List`.
Expand All @@ -11,21 +11,54 @@ Take a nested Map and flatten it with delimited keys. Entirely based on node.js
```dart
import 'package:flat/flat.dart';
flatten({
'key1': {'keyA': 'valueI'},
'key2': {'keyB': 'valueII'},
'key3': {
'a': {
'b': {'c': 2}
}
}
flatten(
{
"a": 1,
"list1": ["item1", "item2"],
"f": {
"list2": ["item3", "item4", "item5"],
"g": 2,
"h": true,
"j": "text",
},
},
);
// {
// "a": 1,
// "list1.0": "item1",
// "list1.1": "item2",
// "f.list2.0": "item3",
// "f.list2.1": "item4",
// "f.list2.2": "item5",
// "f.g": 2,
// "f.h": true,
// "f.j": "text"
// }
unflatten({
"a": 1,
"list1.0": "item1",
"list1.1": "item2",
"f.list2.0": "item3",
"f.list2.1": "item4",
"f.list2.2": "item5",
"f.g": 2,
"f.h": true,
"f.j": "text"
});
// {
// 'key1.keyA': 'valueI',
// 'key2.keyB': 'valueII',
// 'key3.a.b.c': 2
// };
// "a": 1,
// "list1": ["item1", "item2"],
// "f": {
// "list2": ["item3", "item4", "item5"],
// "g": 2,
// "h": true,
// "j": "text",
// },
// }
```

## Options
Expand Down
23 changes: 19 additions & 4 deletions example/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,24 @@ import 'package:flat/flat.dart';
// ignore_for_file: avoid_print

void main() {
final flat = flatten({
"a": 1,
"b": {"c": 2}
});
final flat = flatten(
{
"a": 1,
"list1": ["item1", "item2"],
"f": {
"list2": ["item3", "item4", "item5"],
"list3": [
{"item3": "item47"},
{"item4": "item48"},
{"item5": "item49"},
],
"g": 2,
"h": true,
"j": "text",
},
},
);
print(flat);

print(unflatten(flat));
}
52 changes: 2 additions & 50 deletions lib/flat.dart
Original file line number Diff line number Diff line change
@@ -1,50 +1,2 @@
/// Flatten a nested Map into a single level map
///
/// If no [delimiter] is specified, will separate depth levels by `.`.
///
/// If you don't want to flatten arrays (with 0, 1,... indexes),
/// use [safe] mode.
///
/// To avoid circular reference issues or huge calculations,
/// you can specify the [maxDepth] the function will traverse.
Map<String, dynamic> flatten(
Map<String, dynamic> target, {
String delimiter = ".",
bool safe = false,
int? maxDepth,
}) {
final result = <String, dynamic>{};

void step(
Map<String, dynamic> obj, [
String? previousKey,
int currentDepth = 1,
]) {
obj.forEach((key, value) {
final newKey = previousKey != null ? "$previousKey$delimiter$key" : key;

if (maxDepth != null && currentDepth >= maxDepth) {
result[newKey] = value;
return;
}
if (value is Map<String, dynamic>) {
return step(value, newKey, currentDepth + 1);
}
if (value is List && !safe) {
return step(
_listToMap(value),
newKey,
currentDepth + 1,
);
}
result[newKey] = value;
});
}

step(target);

return result;
}

Map<String, T> _listToMap<T>(List<T> list) =>
list.asMap().map((key, value) => MapEntry(key.toString(), value));
export './src/flatten.dart';
export './src/unflatten.dart';
50 changes: 50 additions & 0 deletions lib/src/flatten.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/// Flatten a nested Map into a single level map
///
/// If no [delimiter] is specified, will separate depth levels by `.`.
///
/// If you don't want to flatten arrays (with 0, 1,... indexes),
/// use [safe] mode.
///
/// To avoid circular reference issues or huge calculations,
/// you can specify the [maxDepth] the function will traverse.
Map<String, dynamic> flatten(
Map<String, dynamic> target, {
String delimiter = ".",
bool safe = false,
int? maxDepth,
}) {
final result = <String, dynamic>{};

void step(
Map<String, dynamic> obj, [
String? previousKey,
int currentDepth = 1,
]) {
obj.forEach((key, value) {
final newKey = previousKey != null ? "$previousKey$delimiter$key" : key;

if (maxDepth != null && currentDepth >= maxDepth) {
result[newKey] = value;
return;
}
if (value is Map<String, dynamic>) {
return step(value, newKey, currentDepth + 1);
}
if (value is List && !safe) {
return step(
_listToMap(value),
newKey,
currentDepth + 1,
);
}
result[newKey] = value;
});
}

step(target);

return result;
}

Map<String, T> _listToMap<T>(List<T> list) =>
list.asMap().map((key, value) => MapEntry(key.toString(), value));
77 changes: 77 additions & 0 deletions lib/src/unflatten.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/// Unflatten a map with keys such as `a.b.c: value` to a nested Map structure `{a: {b: {c: value}}}`.
///
/// If no [delimiter] is specified, will separate depth levels by `.`.
///
/// Unflattens arrays given that the keys are integers.
///
/// Throws [ArgumentError] if any key is already a Map or List.
///
/// Throws [ArgumentError] if there are conflicting keys.
Map<String, dynamic> unflatten(
Map<String, dynamic> flatMap, {
String delimiter = ".",
}) {
final Map<String, dynamic> result = {};

flatMap.forEach((key, value) {
if (value is Map) {
throw ArgumentError('Expected flat map, but key "$key" is a Map');
}
if (value is List) {
throw ArgumentError('Expected flat map, but key "$key" is a List');
}
});

flatMap.forEach((key, value) {
final keys = key.split(delimiter);
dynamic current = result;

for (int i = 0; i < keys.length; i++) {
final k = keys[i];
if (i == keys.length - 1) {
// Last key, assign the value
if (_isInteger(k)) {
final int index = int.parse(k);
while ((current as List).length <= index) {
current.add(null); // Padding the array
}
current[index] = value;
} else {
if ((current as Map).containsKey(k)) {
throw ArgumentError('Cannot unflatten, key "$k" already exists');
}
current[k] = value;
}
} else {
// Not the last key, we might need to create a map or array
if (_isInteger(k)) {
final int index = int.parse(k);
while ((current as List).length <= index) {
current.add(null); // Padding the array
}
// Ensure that we have a Map at the index if the next key is not an integer
if (!_isInteger(keys[i + 1]) &&
(current[index] == null || current[index] is! Map)) {
current[index] = {};
}
current = current[index];
} else {
if ((current as Map)[k] == null) {
// Next key will tell us whether to create a list or a map
current[k] = _isInteger(keys[i + 1]) ? [] : <String, dynamic>{};
}
current = current[k];
}
}
}
});

return result;
}

bool _isInteger(String? value) {
if (value == null) {
return false;
}
return int.tryParse(value) != null;
}
Loading

0 comments on commit efbb316

Please sign in to comment.