Strings are a sequence of characters. Dart represents strings as a sequence of Unicode UTF-16 code units. Unicode is a format that defines a unique numeric value for each letter, digit, and symbol.
Since a Dart string is a sequence of UTF-16 code units, 32-bit Unicode values within a string are represented using a special syntax. A rune is an integer representing a Unicode code point.
The String class in the dart:core library provides mechanisms to access runes. String code units / runes can be accessed in three ways −
Using String.codeUnitAt() function
Using String.codeUnits property
Using String.runes property
String.codeUnitAt() Function
Code units in a string can be accessed through their indexes. Returns the 16-bit UTF-16 code unit at the given index.
This property returns an iterable of Unicode code-points of this string.Runes extends iterable.
Syntax
String.runes
Example
void main(){
"A string".runes.forEach((int rune) {
var character=new String.fromCharCode(rune);
print(character);
});
}
It will produce the following output −
A
s
t
r
i
n
g
Unicode code points are usually expressed as \uXXXX, where XXXX is a 4-digit hexadecimal value. To specify more or less than 4 hex digits, place the value in curly brackets. One can use the constructor of the Runes class in the dart:core library for the same.
Symbols in Dart are opaque, dynamic string name used in reflecting out metadata from a library. Simply put, symbols are a way to store the relationship between a human readable string and a string that is optimized to be used by computers.
Reflection is a mechanism to get metadata of a type at runtime like the number of methods in a class, the number of constructors it has or the number of parameters in a function. You can even invoke a method of the type which is loaded at runtime.
In Dart reflection specific classes are available in the dart:mirrors package. This library works in both web applications and command line applications.
Syntax
Symbol obj = new Symbol('name');
// expects a name of class or function or library to reflect
The name must be a valid public Dart member name, public constructor name, or library name.
Example
Consider the following example. The code declares a class Foo in a library foo_lib. The class defines the methods m1, m2, and m3.
Foo.dart
library foo_lib;
// libarary name can be a symbol
class Foo {
// class name can be a symbol
m1() {
// method name can be a symbol
print("Inside m1");
}
m2() {
print("Inside m2");
}
m3() {
print("Inside m3");
}
}
The following code loads Foo.dart library and searches for Foo class, with help of Symbol type. Since we are reflecting the metadata from the above library the code imports dart:mirrors library.
FooSymbol.dart
import 'dart:core';
import 'dart:mirrors';
import 'Foo.dart';
main() {
Symbol lib = new Symbol("foo_lib");
//library name stored as Symbol
Symbol clsToSearch = new Symbol("Foo");
// class name stored as Symbol
if(checkIf_classAvailableInlibrary(lib, clsToSearch))
// searches Foo class in foo_lib library
print("Found Library");
print("checkng...class details..");
print("No of classes found is : ${libMirror.declarations.length}");
libMirror.declarations.forEach((s, d) => print(s));
if (libMirror.declarations.containsKey(className)) return true;
return false;
}
}
Note that the line libMirror.declarations.forEach((s, d) => print(s)); will iterate across every declaration in the library at runtime and prints the declarations as type of Symbol.
This code should produce the following output −
Found Library
checkng...class details..
No of classes found is : 1
Symbol("Foo") // class name displayed as symbol
class found.
Example: Display the number of instance methods of a class
Let us now consider displaying the number of instance methods in a class. The predefined class ClassMirror helps us to achieve the same.
import 'dart:core';
import 'dart:mirrors';
import 'Foo.dart';
main() {
Symbol lib = new Symbol("foo_lib");
Symbol clsToSearch = new Symbol("Foo");
reflect_InstanceMethods(lib, clsToSearch);
}
void reflect_InstanceMethods(Symbol libraryName, Symbol className) {
MirrorSystem mirrorSystem = currentMirrorSystem();
LibraryMirror libMirror = mirrorSystem.findLibrary(libraryName);
if (libMirror != null) {
print("Found Library");
print("checkng...class details..");
print("No of classes found is : ${libMirror.declarations.length}");
libMirror.declarations.forEach((s, d) => print(s));
if (libMirror.declarations.containsKey(className)) print("found class");
ClassMirror classMirror = libMirror.declarations[className];
print("No of instance methods found is ${classMirror.instanceMembers.length}");
classMirror.instanceMembers.forEach((s, v) => print(s));
}
}
This code should produce the following output −
Found Library
checkng...class details..
No of classes found is : 1
Symbol("Foo")
found class
No of instance methods found is 8
Symbol("==")
Symbol("hashCode")
Symbol("toString")
Symbol("noSuchMethod")
Symbol("runtimeType")
Symbol("m1")
Symbol("m2")
Symbol("m3")
Convert Symbol to String
You can convert the name of a type like class or library stored in a symbol back to string using MirrorSystem class. The following code shows how you can convert a symbol to a string.
import 'dart:mirrors';
void main(){
Symbol lib = new Symbol("foo_lib");
String name_of_lib = MirrorSystem.getName(lib);
print(lib);
print(name_of_lib);
}
The Map object is a simple key/value pair. Keys and values in a map may be of any type. A Map is a dynamic collection. In other words, Maps can grow and shrink at runtime.
Maps can be declared in two ways −
Using Map Literals
Using a Map constructor
Declaring a Map using Map Literals
To declare a map using map literals, you need to enclose the key-value pairs within a pair of curly brackets “{ }”.
Here is its syntax −
var identifier = { key1:value1, key2:value2 [,…..,key_n:value_n] }
Declaring a Map using a Map Constructor
To declare a Map using a Map constructor, we have two steps. First, declare the map and second, initialize the map.
The syntax to declare a map is as follows −
var identifier = new Map()
Now, use the following syntax to initialize the map −
map_name[key] = value
Example: Map Literal
void main() {
var details = {'Usrname':'tom','Password':'pass@123'};
print(details);
}
So far, we have not discussed any associative data structures, i.e., data structures that can associate a certain value (or multiple values) to a key. Different languages call these features with different names like dictionaries, hashes, associative arrays, etc.
In Elixir, we have two main associative data structures: keyword lists and maps. In this chapter, we will focus on Keyword lists.
In many functional programming languages, it is common to use a list of 2-item tuples as the representation of an associative data structure. In Elixir, when we have a list of tuples and the first item of the tuple (i.e. the key) is an atom, we call it a keyword list. Consider the following example to understand the same −
list = [{:a, 1}, {:b, 2}]
Elixir supports a special syntax for defining such lists. We can place the colon at the end of each atom and get rid of the tuples entirely. For example,
The above program generates the following result −
1
Keyword lists have three special characteristics −
Keys must be atoms.
Keys are ordered, as specified by the developer.
Keys can be given more than once.
In order to manipulate keyword lists, Elixir provides the Keyword module. Remember, though, keyword lists are simply lists, and as such they provide the same linear performance characteristics as lists. The longer the list, the longer it will take to find a key, to count the number of items, and so on. For this reason, keyword lists are used in Elixir mainly as options. If you need to store many items or guarantee one-key associates with a maximum one-value, you should use maps instead.
Accessing a key
To access values associated with a given key, we use the Keyword.get function. It returns the first value associated with the given key. To get all the values, we use the Keyword.get_values function. For example −
A linked list is a heterogeneous list of elements that are stored at different locations in memory and are kept track of by using references. Linked lists are data structures especially used in functional programming.
Elixir uses square brackets to specify a list of values. Values can be of any type −
[1, 2, true, 3]
When Elixir sees a list of printable ASCII numbers, Elixir will print that as a char list (literally a list of characters). Whenever you see a value in IEx and you are not sure what it is, you can use the i function to retrieve information about it.
This will give you a concatenated string in the first case and a subtracted string in the second. The above program generates the following result −
[1, 2, 3, 4, 5, 6]
[1, 2, 3, true]
Head and Tail of a List
The head is the first element of a list and the tail is the remainder of a list. They can be retrieved with the functions hd and tl. Let us assign a list to a variable and retrieve its head and tail.
list = [1, 2, 3]
IO.puts(hd(list))
IO.puts(tl(list))
This will give us the head and tail of the list as output. The above program generates the following result −
1
[2, 3]
Note − Getting the head or the tail of an empty list is an error.
Other List functions
Elixir standard library provides a whole lot of functions to deal with lists. We will have a look at some of those here.
S.no.
Function Name and Description
1
delete(list, item)Deletes the given item from the list. Returns a list without the item. If the item occurs more than once in the list, just the first occurrence is removed.
2
delete_at(list, index)Produces a new list by removing the value at the specified index. Negative indices indicate an offset from the end of the list. If index is out of bounds, the original list is returned.
3
first(list)Returns the first element in list or nil if list is empty.
4
flatten(list)Flattens the given list of nested lists.
5
insert_at(list, index, value)Returns a list with value inserted at the specified index. Note that index is capped at the list length. Negative indices indicate an offset from the end of the list.
6
last(list)Returns the last element in list or nil if list is empty.
Tuples
Tuples are also data structures which store a number of other structures within them. Unlike lists, they store elements in a contiguous block of memory. This means accessing a tuple element per index or getting the tuple size is a fast operation. Indexes start from zero.
Elixir uses curly brackets to define tuples. Like lists, tuples can hold any value −
{:ok, "hello"}
Length of a Tuple
To get the length of a tuple, use the tuple_size function as in the following program −
This will create and return a new tuple: {:ok, “Hello”, :world}
Inserting a Value
To insert a value at a given position, we can either use the Tuple.insert_at function or the put_elem function. Consider the following example to understand the same −
Notice that put_elem and insert_at returned new tuples. The original tuple stored in the tuple variable was not modified because Elixir data types are immutable. By being immutable, Elixir code is easier to reason about as you never need to worry if a particular code is mutating your data structure in place.
Tuples vs. Lists
What is the difference between lists and tuples?
Lists are stored in memory as linked lists, meaning that each element in a list holds its value and points to the following element until the end of the list is reached. We call each pair of value and pointer a cons cell. This means accessing the length of a list is a linear operation: we need to traverse the whole list in order to figure out its size. Updating a list is fast as long as we are prepending elements.
Tuples, on the other hand, are stored contiguously in memory. This means getting the tuple size or accessing an element by index is fast. However, updating or adding elements to tuples is expensive because it requires copying the whole tuple in memory.
A char list is nothing more than a list of characters. Consider the following program to understand the same.
IO.puts('Hello')
IO.puts(is_list('Hello'))
The above program generates the following result −
Hello
true
Instead of containing bytes, a char list contains the code points of the characters between single-quotes. So while the double-quotes represent a string (i.e. a binary), singlequotes represent a char list (i.e. a list). Note that IEx will generate only code points as output if any of the chars is outside the ASCII range.
Char lists are used mostly when interfacing with Erlang, in particular old libraries that do not accept binaries as arguments. You can convert a char list to a string and back by using the to_string(char_list) and to_char_list(string) functions −
The above program generates the following result −
true
true
NOTE − The functions to_string and to_char_list are polymorphic, i.e., they can take multiple types of input like atoms, integers and convert them to strings and char lists respectively.
In this chapter, we will discuss how to carry out some basic operations on Lists, such as −
Sr.No
Basic Operation & Description
1
Inserting Elements into a ListMutable Lists can grow dynamically at runtime. The List.add() function appends the specified value to the end of the List and returns a modified List object.
2
Updating a listLists in Dart can be updated by −Updating The IndexUsing the List.replaceRange() function
3
Removing List itemsThe following functions supported by the List class in the dart:core library can be used to remove the item(s) in a List.
A very commonly used collection in programming is an array. Dart represents arrays in the form of List objects. A List is simply an ordered group of objects. The dart:core library provides the List class that enables creation and manipulation of lists.
The logical representation of a list in Dart is given below −
test_list − is the identifier that references the collection.
The list contains in it the values 12, 13, and 14. The memory blocks holding these values are known as elements.
Each element in the List is identified by a unique number called the index. The index starts from zero and extends up to n-1 where n is the total number of elements in the List. The index is also referred to as the subscript.
Lists can be classified as −
Fixed Length List
Growable List
Let us now discuss these two types of lists in detail.
Fixed Length List
A fixed length list’s length cannot change at runtime. The syntax for creating a fixed length list is as given below −
Step 1 − Declaring a list
The syntax for declaring a fixed length list is given below −
var list_name = new List(initial_size)
The above syntax creates a list of the specified size. The list cannot grow or shrink at runtime. Any attempt to resize the list will result in an exception.
Step 2 − Initializing a list
The syntax for initializing a list is as given below −
lst_name[index] = value;
Example
void main() {
var lst = new List(3);
lst[0] = 12;
lst[1] = 13;
lst[2] = 11;
print(lst);
}
It will produce the following output −
[12, 13, 11]
Growable List
A growable list’s length can change at run-time. The syntax for declaring and initializing a growable list is as given below −
Step 1 − Declaring a List
var list_name = [val1,val2,val3]
--- creates a list containing the specified values
OR
var list_name = new List()
--- creates a list of size zero
Step 2 − Initializing a List
The index / subscript is used to reference the element that should be populated with a value. The syntax for initializing a list is as given below −
list_name[index] = value;
Example
The following example shows how to create a list of 3 elements.Live Demo
void main() {
var num_list = [1,2,3];
print(num_list);
}
It will produce the following output −
[1, 2, 3]
Example
The following example creates a zero-length list using the empty List() constructor. The add() function in the List class is used to dynamically add elements to the list.
void main() {
var lst = new List();
lst.add(12);
lst.add(13);
print(lst);
}
It will produce the following output −
[12, 13]
List Properties
The following table lists some commonly used properties of the List class in the dart:core library.
Sr.No
Methods & Description
1
firstReturns the first element in the list.
2
isEmptyReturns true if the collection has no elements.
3
isNotEmptyReturns true if the collection has at least one element.
4
lengthReturns the size of the list.
5
lastReturns the last element in the list.
6
reversedReturns an iterable object containing the lists values in the reverse order.
7
SingleChecks if the list has only one element and returns it.
Dart provides an inbuilt support for the Boolean data type. The Boolean data type in DART supports only two values – true and false. The keyword bool is used to represent a Boolean literal in DART.
The syntax for declaring a Boolean variable in DART is as given below −
Unlike JavaScript, the Boolean data type recognizes only the literal true as true. Any other value is considered as false. Consider the following example −
var str = 'abc';
if(str) {
print('String is not empty');
} else {
print('Empty String');
}
The above snippet, if run in JavaScript, will print the message ‘String is not empty’ as the if construct will return true if the string is not empty.
However, in Dart, str is converted to false as str != true. Hence the snippet will print the message ‘Empty String’ (when run in unchecked mode).
Example
The above snippet if run in checked mode will throw an exception. The same is illustrated below −
void main() {
var str = 'abc';
if(str) {
print('String is not empty');
} else {
print('Empty String');
}
}
It will produce the following output, in Checked Mode −
Unhandled exception:
type 'String' is not a subtype of type 'bool' of 'boolean expression' where
String is from dart:core
bool is from dart:core
#0 main (file:///D:/Demos/Boolean.dart:5:6)
#1 _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:261)
#2 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:148)
It will produce the following output, in Unchecked Mode −
Empty String
Note − The WebStorm IDE runs in checked mode, by default.
Strings in Elixir are inserted between double quotes, and they are encoded in UTF-8. Unlike C and C++ where the default strings are ASCII encoded and only 256 different characters are possible, UTF-8 consists of 1,112,064 code points. This means that UTF-8 encoding consists of those many different possible characters. Since the strings use utf-8, we can also use symbols like: ö, ł, etc.
Create a String
To create a string variable, simply assign a string to a variable −
str = "Hello world"
To print this to your console, simply call the IO.puts function and pass it the variable str −
str = str = "Hello world"
IO.puts(str)
The above program generates the following result −
Hello World
Empty Strings
You can create an empty string using the string literal, “”. For example,
a = ""
if String.length(a) === 0 do
IO.puts("a is an empty string")
end
The above program generates the following result.
a is an empty string
String Interpolation
String interpolation is a way to construct a new String value from a mix of constants, variables, literals, and expressions by including their values inside a string literal. Elixir supports string interpolation, to use a variable in a string, when writing it, wrap it with curly braces and prepend the curly braces with a ‘#’ sign.
For example,
x = "Apocalypse"
y = "X-men #{x}"
IO.puts(y)
This will take the value of x and substitute it in y. The above code will generate the following result −
X-men Apocalypse
String Concatenation
We have already seen the use of String concatenation in previous chapters. The ‘<>’ operator is used to concatenate strings in Elixir. To concatenate 2 strings,
x = "Dark"
y = "Knight"
z = x <> " " <> y
IO.puts(z)
The above code generates the following result −
Dark Knight
String Length
To get the length of the string, we use the String.length function. Pass the string as a parameter and it will show you its size. For example,
IO.puts(String.length("Hello"))
When running above program, it produces following result −
5
Reversing a String
To reverse a string, pass it to the String.reverse function. For example,
IO.puts(String.reverse("Elixir"))
The above program generates the following result −
rixilE
String Comparison
To compare 2 strings, we can use the == or the === operators. For example,
var_1 = "Hello world"
var_2 = "Hello Elixir"
if var_1 === var_2 do
IO.puts("#{var_1} and #{var_2} are the same")
else
IO.puts("#{var_1} and #{var_2} are not the same")
end
The above program generates the following result −
Hello world and Hello elixir are not the same.
String Matching
We have already seen the use of the =~ string match operator. To check if a string matches a regex, we can also use the string match operator or the String.match? function. For example,
The above program generates the following result −
true
false
This same can also be achieved by using the =~ operator. For example,
IO.puts("foo" =~ ~r/foo/)
The above program generates the following result −
true
String Functions
Elixir supports a large number of functions related to strings, some of the most used are listed in the following table.
Sr.No.
Function and its Purpose
1
at(string, position)Returns the grapheme at the position of the given utf8 string. If position is greater than string length, then it returns nil
2
capitalize(string)Converts the first character in the given string to uppercase and the remainder to lowercase
3
contains?(string, contents)Checks if string contains any of the given contents
4
downcase(string)Converts all characters in the given string to lowercase
5
ends_with?(string, suffixes)Returns true if string ends with any of the suffixes given
6
first(string)Returns the first grapheme from a utf8 string, nil if the string is empty
7
last(string)Returns the last grapheme from a utf8 string, nil if the string is empty
8
replace(subject, pattern, replacement, options \\ [])Returns a new string created by replacing occurrences of pattern in subject with replacement
9
slice(string, start, len)Returns a substring starting at the offset start, and of length len
10
split(string)Divides a string into substrings at each Unicode whitespace occurrence with leading and trailing whitespace ignored. Groups of whitespace are treated as a single occurrence. Divisions do not occur on non-breaking whitespace
11
upcase(string)Converts all characters in the given string to uppercase
Binaries
A binary is just a sequence of bytes. Binaries are defined using << >>. For example:
<< 0, 1, 2, 3 >>
Of course, those bytes can be organized in any way, even in a sequence that does not make them a valid string. For example,
<< 239, 191, 191 >>
Strings are also binaries. And the string concatenation operator <> is actually a Binary concatenation operator:
IO.puts(<< 0, 1 >> <> << 2, 3 >>)
The above code generates the following result −
<< 0, 1, 2, 3 >>
Note the ł character. Since this is utf-8 encoded, this character representation takes up 2 bytes.
Since each number represented in a binary is meant to be a byte, when this value goes up from 255, it is truncated. To prevent this, we use size modifier to specify how many bits we want that number to take. For example −
The above program will generate the following result −
<< 0 >>
<< 1, 0 >>
We can also use the utf8 modifier, if a character is code point then, it will be produced in the output; else the bytes −
IO.puts(<< 256 :: utf8 >>)
The above program generates the following result −
Ā
We also have a function called is_binary that checks if a given variable is a binary. Note that only variables which are stored as multiples of 8bits are binaries.
Bitstrings
If we define a binary using the size modifier and pass it a value that is not a multiple of 8, we end up with a bitstring instead of a binary. For example,
The above program generates the following result −
<< 1::size(1) >>
false
true
This means that variable bs is not a binary but rather a bitstring. We can also say that a binary is a bitstring where the number of bits is divisible by 8. Pattern matching works on binaries as well as bitstrings in the same way.