Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for unflatten function #9

Merged
merged 9 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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