README.md

# Data Structure Agnostic Library for Erlang

A simple library for doing common things with common Erlang data structures
(proplists, dicts, and maps for now). This will convert from one structure to
another, and can allow you to "just do the damn thing" without worrying about
the form of the underlying data structure.

This is also designed to help the user incrementally convert an application
from one data structure to another. For example, if you have a legacy
application that uses proplists as a primary data structure, and you want to
upgrade to using maps instead, this is a handy tool for making that conversion,
since the calls to `ds` don't have to change.


## A few things worth noting:

  * The primary module here is `ds`.  So `ds:get` is a function call to get
	values.  
  * Calls are intended to be short (less typing).  So `ds:get` instead of
	`proplist:get_value`.
  * Currently supported data structures are: maps, dicts, and lists of
	`{Key,Value}` tuples (generally referred to as proplists here, but these
	are not *exactly* proplists [0]).
  * Calls work regardless of the supported data structure.
  * If the return value is intended to be a modified version of the provided
	data structure (e.g. `ds:set(Obj, Key, Value)`), then the return value will
	be of the same type.  So from that example, if `Obj` is a map, the return
	value is a map.  If `Obj` is a dict, then the return value is a dict.
  * In *most* circumstances, the `Obj` argument (the Data Structure) is the
	first argument in a function, rather than the norm in Erlang where it is
	typically the last argument. For example: `proplists:get_value(Key, Obj)`
	vs `ds:get(Obj, Key)`.
  * Also, in *most* circumstances, if there is a `Default` value (to return if
	a key isn't found) that `Default` argument will be the last argument of the
	function. For example: `ds:get(Obj, Key, DefaultValue)`.
  * **Controversial Item**: The standard return value if something isn't found
	is an empty string/empty list (`""` or `[]`).  If this is a major sticking
	point for users, this can be changed to a config setting.

## Function Reference

**`Obj` and the term "object" will be used to represent the provided data
structure, even though I'm well aware none of these are true `Objects` in the
Object-oriented sense.**

**Optional Arguments are presented in `[Brackets]`**

### Getting, Setting, and Deleting (one value)

  * `ds:get(Obj, Key, [Default])`: Get the value associated with `Key` from
	`Obj`. Return the found value, or return `Default` if no value is found,
	and if `Default` is not provided, return `""`
  * `ds:set(Obj, Key, Value)`: In `Obj` set the value associated with `Key` to
	`Value`, and return the modified object.

### Getting and Setting (multiple values)

  * `ds:get_list(Obj, ListOfKeys, [Default])`: For each item in `ListOfKeys`
	return a the associated value (or if not found, return `Default for that
	item, or if `Default` is not provided, return `""` for that key). Example:
	`[A,B] = ds:get_list(Obj, [a,b])`
  * `ds:set(Obj, ListOfKeyValues)`: `ListOfKeyValues` is a list of
	`{Key,Value}` tuples, and perform a mass-update by setting each value
	associated with `Key` to `Value` in the provided `Obj` and return the
	modified object.


### Deleting and Filtering Values

  * `ds:delete(Obj, Key)` - Remove the entry associated with `Key` from `Obj`.
	Returns the modified object.
  * `ds:delete_list(Obj, ListOfKeys)` - Removes all the entries associated with
	each `Key` in `ListOfKeys` from `Obj`. Returns the modified object.
  * `ds:keep(Obj, ListOfKeys)` - Removes all entries from `Obj` except for
	those whose keys are listed in `ListOfKeys`. Returns the modified object.
  * `ds:filter(Obj, Fun)` - Iterates over `Obj` and retains only those entries
	for which `Fun(Key, Value)` returns `true`. Returns the modified object.

### Working with Keys

  * `ds:has_key(Obj, Key)`: Checks if `Obj` contains an entry for `Key`.
	Returns `true` if found, otherwise `false`.
  * `ds:rekey(Obj, OldKey, NewKey)`: Renames `OldKey` to `NewKey` in `Obj`. If
	`NewKey` already exists, its value is overwritten. Returns the modified
	object.
  * `ds:rekey(Obj, ListOfOldAndNewKeys)`: `ListOfOldAndNewKeys` is a list of
	`{OldKey, NewKey}` tuples. Renames each `OldKey` to its corresponding
	`NewKey`. If a `NewKey` already exists, its value is overwritten. Returns
	the modified object.

### Mass Updating

  * `ds:boolize(Obj, ListOfKeys)` - Convert the values in `Obj` associated with
	each `Key` in `ListOfKeys` to a boolean
  * `ds:atomize(Obj, ListOfKeys)` - Convert the values in `Obj` associated with
	each `Key` in `ListOfKeys` to an atom.
  * `ds:map(Obj, Fun)` - Run `Fun` on every entry in `Obj` and set each entry's
	new value to the return value from `Fun`. `Fun` can be defined as either
	`Fun(Value) -> NewValue` or `Fun(Key, Value) -> NewValue`, either way, the
	value of each entry will be set to `NewValue`.
  * `ds:update(Obj, ListOfKeys, Fun)` - Update the values in `Obj` associated
	with each `Key` in `ListOfKeys` by running each associated value through
	the provided `Fun(Value)`. For example, to convert a handful of values to
	their integer forms (or `undefined` if not parseable), you could implement
	it like this: `ds:update(Obj, ListOfKeys, fun(X) -> try list_to_integer(X)
	catch _:_ -> undefined end)`

### Transforming: Updating on Steroids

  * `ds:transform(Obj, TransformList)` - Run many different updates on `Obj`.
	`TransformList` is a list of `{Operation, ListOfKeys}` tuples. `Operation`
	can be a function that takes a single `Value` and returns a new `Value`, or
	`Operation` can be any of the following terms: `atomize`, `boolize`,
	`date`, `unixtime`, `now`, `{date, DateFormat}`. `ListOfKeys` is a list of
	keys to convert. Returns the `Obj` with all the updates applied.  Bear in
	mind, the date and time related functions all assume
	[qdate](https://github.com/choptastic/qdate)
	([@hex.pm](https://hex.pm/packages/qdate)) is installed.  `qdate`, however
	is not an automatic dependency because the rest of the module works without
	it.

### Conversion and Type-Checking

  * `ds:type(Obj)` - returns the type of data structure (`map`, `dict`, or
	`list`).
  * `ds:to_list(Obj)` - Convert `Obj` to a proplist. If `Obj` is already a
	list, it returns it unchanged.
  * `ds:to_map(Obj)`: Convert `Obj` to a map. If `Obj` is already a map,
	returns it unchanged.
  * `ds:to_dict(Obj)`: Convert `Obj` to a dict. If `Obj` is already a dict,
	returns it unchanged.

### Comparison Helpers for Sorting lists of Objects

  * `ds:compare(ObjA, ObjB, SortCriteria)` - `SortCriteria` can be a single
	`Key` or `{Operation, Key}` tuple, or it can be a list of `Key` or
	`{Operation, Key}` tuples. `Operation` can be the atoms `<` or `asc` (for
	ascending)  or the atoms `>` or `desc` (for descending). `Operation` can
	also be a function of arity 2 (meaning, it takes 2 arguments). These
	arguments will be the values associated with `Key` of from `ObjA` and
	`ObjB`. If no `Operation` is provided (that is, if an item in
	`SortCriteria` is not a 2-tuple, it's treated as a `Key` and `asc` is used
	(sort in ascending order)`

### Merging Objects

  * `ds:merge(ListOfObjects)` - Merge the list of objects together. Returns
	`{Merged, Unmerged}`.  Blank values (`""`, `<<>>`, `undefined`) will be
	replaced by any other non-blank items. If there are conflicts (where
	several objects have values for the same key), then those values will be
	assembled into a list of values and returned
  * `ds:merge(ObjA, ObjB)` - A shortcut for `ds:merge([ObjA, ObjB)])`. It just
	merges two objects.
  * `ds:guess_merge(ListOfObjects)` - This works similarly to `ds:merge` except
	that this will return a single fully-merged object. Unlike `ds:merge`, if
	this encounters a conflict, it goes with the first non-blank value
	encountered.
  * `ds:guess_merge(ObjA, ObjB)` - A shortcut for `ds:guess_merge([ObjA, ObjB)])`.
    It just merges two objects.

## Add to your rebar.config's deps section

```erlang
{deps, [
	erlang_ds
]}.
```

## History and Philosophy

This library came out of a desire to have a single module with short function
calls to work with proplists (bear in mind, this was made before Erlang had a
native `map` data structure).  In most circumstances, these days, maps are the
superior data structure these days.  But there is legacy code using proplists
or dicts for things, or a mix of all three.

On top of that, there are a lot of situations where the functionality provided
by one module is insufficient for my needs. For example:

* While `maps` and `dict` both support merging, `proplists` does not.
* While `ds:transform` and `ds:rekey` can both be implemented with
  `Module:map`, `map` is clunky for both.
* While `map` supports getting multiple things per line with pattern matching
  (`#{key:=Value, key2:=Value2} = Map`), this is not supported by proplists or
  dicts. Also, the `map` syntax above will crash if one of the values isn't
  present, necessitating the `maps:get/3` function anyway.
* The inconsistencies of the modules makes me constantly forget which module
  does it which way.  `get` and `set`, for example, which are the most common
  operations in any of my codebases (and I suspect the most common in yours as
  well) are implemented in `maps` as `get` and `put`, in `dict` as `find` and
  `store`, in `proplists` as `get_value` and *not implemented* (basically,
  just delete the old value and prepend the new `{Key,Value}` tuple), or with
  the `lists` module as `keyfind` and `keystore`.

None of this is to criticize the Erlang team - they do incredible work, and
they can't do everything for everyone, nor should every API for every data
structure be exactly the same. Indeed, the `lists:key*` functions are
individually more flexible for lists of tuples of all sizes (not just the
`{Key, Value}` tuples. But for someone using mostly `{Key, Value}` tuples,
`lists:key:*` are overkill (but are very handy to use under the hood for the
functions in Erlang DS).

Perhaps my use cases are not useful to the community at
large, but I suspect there are others out there like me who need
or want this kind of utility.

Further, I acknowledge that there likely some inefficiencies here, or methods
where I might be using a less-than ideal implementation, and for that, I'm open
to comments and pull requests to improve the software.

## Final Comment

Erlang DS was originally called `sigma_proplist` using a module called just
`pl`.  "Sigma" is just a reference to the author's software business, [Sigma
Star Systems](https://sigma-star.com).  The project was renamed to "Erlang DS"
when it added support for other data structures. The original `sigma_proplists`
has been running in production for almost 10 years before converting it to
`Erlang DS`.

## About

Author: [Jesse Gumm](https://jessegumm.com)

Copyright 2013-2023

MIT License