Skip to content

Library contains some routines for work with fuzzy logic operators, fuzzy datasets and fuzzy scales. See article (ru): https://math-n-algo.blogspot.com/2014/08/FuzzyClassificator.html#chapter_3

License

Notifications You must be signed in to change notification settings

devopshq/FuzzyRoutines

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FuzzyRoutines

Library contains some routines for work with fuzzy logic operators, fuzzy datasets and fuzzy scales.

FuzzyRoutines build status FuzzyRoutines on PyPI FuzzyRoutines license

Index:

Install

You can install FuzzyRoutines using pip:

pip install fuzzyroutines [--upgrade] [--pre]

or using setuptools to build local version:

git clone https://github.com/devopshq/FuzzyRoutines.git
cd FuzzyRoutines
python setup.py install

After installing you can check the version of the FuzzyRoutines library:

pip show fuzzyroutines

Usage examples

You can see and run Example.py script:

cd fuzzyroutines
python Examples.py

Example.py contains some examples of working with fuzzy library. Just copying and run examples below. Do not forget to import FuzzyRoutines module before use:

from fuzzyroutines.FuzzyRoutines import *

Work with membership functions

Usage of some membership functions:

mjuPars = {'a': 7, 'b': 4, 'c': 0}  # hyperbolic params example
funct = MFunction(userFunc='hyperbolic', **mjuPars)  # creating instance of hyperbolic function
print('Printing Membership function with parameters: ', funct)

mjuPars = {'a': 0, 'b': 0.3, 'c': 0.4}  # bell params example
funct = MFunction(userFunc='bell', **mjuPars)  # creating instance of bell function
print('Printing Membership function with parameters: ', funct)

mjuPars = {'a': 0, 'b': 1}  # parabolic params example
funct = MFunction(userFunc='parabolic', **mjuPars)  # creating instance of parabolic function
print('Printing Membership function with parameters: ', funct)

mjuPars = {'a': 0.2, 'b': 0.8, 'c': 0.7}  # triangle params example
funct = MFunction(userFunc='triangle', **mjuPars)  # creating instance of triangle function
print('Printing Membership function with parameters: ', funct)

mjuPars = {'a': 0.5, 'b': 0.15}  # exponential params example
funct = MFunction(userFunc='exponential', **mjuPars)  # creating instance of exponential function
print('Printing Membership function with parameters: ', funct)

mjuPars = {'a': 15, 'b': 0.5}  # sigmoidal params example
funct = MFunction(userFunc='sigmoidal', **mjuPars)  # creating instance of sigmoidal function
print('Printing Membership function with parameters: ', funct)

funct = MFunction(userFunc='desirability')  # creating instance of desirability function without parameters
print('Printing Membership function with parameters: ', funct)

mjuPars = {'a': 0.1, 'b': 1, 'c': 0.5, 'd': 0.8}  # trapezium params example
funct = MFunction(userFunc='trapezium', **mjuPars)  # creating instance of trapezium function
print('Printing Membership function with parameters: ', funct)

Output:

Printing Membership function with parameters:  Hyperbolic(x, {"a": 7, "b": 4, "c": 0})
Printing Membership function with parameters:  Bell(x, {"a": 0, "b": 0.3, "c": 0.4})
Printing Membership function with parameters:  Parabolic(x, {"a": 0, "b": 1})
Printing Membership function with parameters:  Triangle(x, {"a": 0.2, "b": 0.8, "c": 0.7})
Printing Membership function with parameters:  Exponential(x, {"a": 0.5, "b": 0.15})
Printing Membership function with parameters:  Sigmoidal(x, {"a": 15, "b": 0.5})
Printing Membership function with parameters:  Desirability(y)
Printing Membership function with parameters:  Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8})

Calculating Trapezium function's values in [0, 1]:

xPar = 0
for i in range(0, 10, 1):
    xPar = (xPar + i) / 10
    res = funct.mju(xPar)  # calculate one value of MF with given parameters
    print('x = {:.1f}, {} = {:1.4f}'.format(xPar, funct, res))

Output:

x = 0.0, Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8}) = 0.0000
x = 0.1, Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8}) = 0.0000
x = 0.2, Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8}) = 0.2750
x = 0.3, Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8}) = 0.5525
x = 0.4, Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8}) = 0.8302
x = 0.5, Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8}) = 1.0000
x = 0.7, Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8}) = 1.0000
x = 0.8, Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8}) = 1.0000
x = 0.9, Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8}) = 0.6173
x = 1.0, Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8}) = 0.0617

Work with fuzzy set

fuzzySet = FuzzySet(funct, (0., 1.))  # creating fuzzy set A = <mju_funct, support_set>
print('Printing fuzzy set after init and before changes:', fuzzySet)
print('Defuz({}) = {:1.2f}'.format(fuzzySet.name, fuzzySet.Defuz()))

changedMjuPars = copy.deepcopy(mjuPars)  # change parameters of membership function with deepcopy example:
changedMjuPars['a'] = 0
changedMjuPars['b'] = 1
changedSupportSet = (0.5, 1)  # change support set
fuzzySet.name = 'Changed fuzzy set'

fuzzySet.mFunction.parameters = changedMjuPars
fuzzySet.supportSet = changedSupportSet

print('New membership function with parameters: ', fuzzySet.mFunction)
print('New support set: ', fuzzySet.supportSet)
print('New value of Defuz({}) = {:1.2f}'.format(fuzzySet.name, fuzzySet.Defuz()))
print('Printing fuzzy set after changes:', fuzzySet)

Output:

Printing fuzzy set after init and before changes: FuzzySet = <Trapezium(x, {"a": 0.1, "b": 1, "c": 0.5, "d": 0.8}), [0.0, 1.0]>
Defuz(FuzzySet) = 0.59
New membership function with parameters:  Trapezium(x, {"a": 0, "b": 1, "c": 0.5, "d": 0.8})
New support set:  (0.5, 1)
New value of Defuz(Changed fuzzy set) = 0.59
Printing fuzzy set after changes: Changed fuzzy set = <Trapezium(x, {"a": 0, "b": 1, "c": 0.5, "d": 0.8}), [0.5, 1]>

Work with fuzzy scales

Fuzzy scale is an ordered set of linguistic variables that looks like this:

S = [{'name': 'name_1', 'fSet': fuzzySet_1}, {'name': 'name_2', 'fSet': fuzzySet_2}, ...]

where name is a linguistic name of fuzzy set, fSet is a user define fuzzy set of FuzzySet type.

scale = FuzzyScale()  # intialize new fuzzy scale with default levels

print('Printing default fuzzy scale in human-readable:', scale)

print('Defuz() of all default levels:')
for item in scale.levels:
    print('Defuz({}) = {:1.2f}'.format(item['name'], item['fSet'].Defuz()))

Output:

Printing default fuzzy scale in human-readable: DefaultScale = {Min, Med, High}
    Minimum = <Hyperbolic(x, {"a": 7, "b": 4, "c": 0}), [0.0, 1.0]>
    Medium = <Bell(x, {"a": 0.35, "b": 0.5, "c": 0.6}), [0.0, 1.0]>
    High = <Triangle(x, {"a": 0.7, "b": 1, "c": 1}), [0.0, 1.0]>
Defuz() of all default levels:
Defuz(Min) = 0.10
Defuz(Med) = 0.55
Defuz(High) = 0.90

Add new fuzzy levels:

print('Define some new levels:')

minFunct = MFunction('hyperbolic', **{'a': 2, 'b': 20, 'c': 0})
levelMin = FuzzySet(membershipFunction=minFunct, supportSet=(0., 0.5), linguisticName='min')
print('Printing Level 1 in human-readable:', levelMin)

medFunct = MFunction('bell', **{'a': 0.4, 'b': 0.55, 'c': 0.7})
levelMed = FuzzySet(membershipFunction=medFunct, supportSet=(0.25, 0.75), linguisticName='med')
print('Printing Level 2 in human-readable:', levelMed)

maxFunct = MFunction('triangle', **{'a': 0.65, 'b': 1, 'c': 1})
levelMax = FuzzySet(membershipFunction=maxFunct, supportSet=(0.7, 1.), linguisticName='max')
print('Printing Level 3 in human-readable:', levelMax)

Output:

Define some new levels:
Printing Level 1 in human-readable: min = <Hyperbolic(x, {"a": 2, "b": 20, "c": 0}), [0.0, 0.5]>
Printing Level 2 in human-readable: med = <Bell(x, {"a": 0.4, "b": 0.55, "c": 0.7}), [0.25, 0.75]>
Printing Level 3 in human-readable: max = <Triangle(x, {"a": 0.65, "b": 1, "c": 1}), [0.7, 1.0]>

Change scale levels:

scale.name = 'New Scale'
scale.levels = [{'name': levelMin.name, 'fSet': levelMin}, {'name': levelMed.name, 'fSet': levelMed},
                {'name': levelMax.name, 'fSet': levelMax}]  # add new ordered set of linguistic variables into scale

print('Changed List of levels as objects:', scale.levels)
print('Printing changed fuzzy scale in human-readable:', scale)

print('Defuz() of all New Scale levels:')
for item in scale.levels:
    print('Defuz({}) = {:1.2f}'.format(item['name'], item['fSet'].Defuz()))

Output:

Changed List of levels as objects: [{'name': 'min', 'fSet': <fuzzyroutines.FuzzyRoutines.FuzzySet object at 0x000001AECB3F17B8>}, {'name': 'med', 'fSet': <fuzzyroutines.FuzzyRoutines.FuzzySet object at 0x000001AECB337D68>}, {'name': 'max', 'fSet': <fuzzyroutines.FuzzyRoutines.FuzzySet object at 0x000001AECB3F18D0>}]
Printing changed fuzzy scale in human-readable: New Scale = {min, med, max}
    min = <Hyperbolic(x, {"a": 2, "b": 20, "c": 0}), [0.0, 0.5]>
    med = <Bell(x, {"a": 0.4, "b": 0.55, "c": 0.7}), [0.25, 0.75]>
    max = <Triangle(x, {"a": 0.65, "b": 1, "c": 1}), [0.7, 1.0]>
Defuz() of all New Scale levels:
Defuz(min) = 0.24
Defuz(med) = 0.61
Defuz(max) = 0.89

Work with Universal Fuzzy Scale

Universal fuzzy scales S_f = {Min, Low, Med, High, Max} pre-defined in UniversalFuzzyScale() class.

uniFScale = UniversalFuzzyScale()
print('Levels of Universal Fuzzy Scale:', uniFScale.levels)
print('Printing scale:', uniFScale)

print('Defuz() of all Universal Fuzzy Scale levels:')
for item in uniFScale.levels:
    print('Defuz({}) = {:1.2f}'.format(item['name'], item['fSet'].Defuz()))

Output:

Levels of Universal Fuzzy Scale: [{'name': 'Min', 'fSet': <fuzzyroutines.FuzzyRoutines.FuzzySet object at 0x000001AECB34F7B8>}, {'name': 'Low', 'fSet': <fuzzyroutines.FuzzyRoutines.FuzzySet object at 0x000001AECB34F198>}, {'name': 'Med', 'fSet': <fuzzyroutines.FuzzyRoutines.FuzzySet object at 0x000001AECB34F048>}, {'name': 'High', 'fSet': <fuzzyroutines.FuzzyRoutines.FuzzySet object at 0x000001AECB34F0F0>}, {'name': 'Max', 'fSet': <fuzzyroutines.FuzzyRoutines.FuzzySet object at 0x000001AECB34F710>}]
Printing scale: FuzzyScale = {Min, Low, Med, High, Max}
    Min = <Hyperbolic(x, {"a": 8, "b": 20, "c": 0}), [0.0, 0.23]>
    Low = <Bell(x, {"a": 0.17, "b": 0.23, "c": 0.34}), [0.17, 0.4]>
    Med = <Bell(x, {"a": 0.34, "b": 0.4, "c": 0.6}), [0.34, 0.66]>
    High = <Bell(x, {"a": 0.6, "b": 0.66, "c": 0.77}), [0.6, 0.83]>
    Max = <Parabolic(x, {"a": 0.77, "b": 0.95}), [0.77, 1.0]>
Defuz() of all Universal Fuzzy Scale levels:
Defuz(Min) = 0.06
Defuz(Low) = 0.29
Defuz(Med) = 0.50
Defuz(High) = 0.71
Defuz(Max) = 0.93

Use Fuzzy() function to looking for level on Fuzzy Scale:

xPar = 0
for i in range(0, 10, 1):
    xPar = (xPar + i) / 10
    res = uniFScale.Fuzzy(xPar)  # calculate fuzzy level for some real values
    print('Fuzzy({:1.1f}, {}) = {}, {}'.format(xPar, uniFScale.name, res['name'], res['fSet']))

Output:

Fuzzy(0.0, FuzzyScale) = Min, Min = <Hyperbolic(x, {"a": 8, "b": 20, "c": 0}), [0.0, 0.23]>
Fuzzy(0.1, FuzzyScale) = Min, Min = <Hyperbolic(x, {"a": 8, "b": 20, "c": 0}), [0.0, 0.23]>
Fuzzy(0.2, FuzzyScale) = Low, Low = <Bell(x, {"a": 0.17, "b": 0.23, "c": 0.34}), [0.17, 0.4]>
Fuzzy(0.3, FuzzyScale) = Low, Low = <Bell(x, {"a": 0.17, "b": 0.23, "c": 0.34}), [0.17, 0.4]>
Fuzzy(0.4, FuzzyScale) = Med, Med = <Bell(x, {"a": 0.34, "b": 0.4, "c": 0.6}), [0.34, 0.66]>
Fuzzy(0.5, FuzzyScale) = Med, Med = <Bell(x, {"a": 0.34, "b": 0.4, "c": 0.6}), [0.34, 0.66]>
Fuzzy(0.7, FuzzyScale) = High, High = <Bell(x, {"a": 0.6, "b": 0.66, "c": 0.77}), [0.6, 0.83]>
Fuzzy(0.8, FuzzyScale) = High, High = <Bell(x, {"a": 0.6, "b": 0.66, "c": 0.77}), [0.6, 0.83]>
Fuzzy(0.9, FuzzyScale) = Max, Max = <Parabolic(x, {"a": 0.77, "b": 0.95}), [0.77, 1.0]>
Fuzzy(1.0, FuzzyScale) = Max, Max = <Parabolic(x, {"a": 0.77, "b": 0.95}), [0.77, 1.0]>

Finding fuzzy level using GetLevelByName() function with exact matching:

print('Finding level by name with exact matching:')

res = uniFScale.GetLevelByName('Min')
print('GetLevelByName(Min, {}) = {}, {}'.format(uniFScale.name, res['name'] if res else 'None', res['fSet'] if res else 'None'))

res = uniFScale.GetLevelByName('High')
print('GetLevelByName(High, {}) = {}, {}'.format(uniFScale.name, res['name'] if res else 'None', res['fSet'] if res else 'None'))

res = uniFScale.GetLevelByName('max')
print('GetLevelByName(max, {}) = {}, {}'.format(uniFScale.name, res['name'] if res else 'None', res['fSet'] if res else 'None'))

Output:

Finding level by name with exact matching:
GetLevelByName(Min, FuzzyScale) = Min, Min = <Hyperbolic(x, {"a": 8, "b": 20, "c": 0}), [0.0, 0.23]>
GetLevelByName(High, FuzzyScale) = High, High = <Bell(x, {"a": 0.6, "b": 0.66, "c": 0.77}), [0.6, 0.83]>
GetLevelByName(max, FuzzyScale) = None, None

Finding fuzzy level using GetLevelByName() function without exact matching:

print('Finding level by name without exact matching:')

res = uniFScale.GetLevelByName('mIn', exactMatching=False)
print("GetLevelByName('mIn', {}) = {}, {}".format(uniFScale.name, res['name'] if res else 'None', res['fSet'] if res else 'None'))

res = uniFScale.GetLevelByName('max', exactMatching=False)
print("GetLevelByName('max', {}) = {}, {}".format(uniFScale.name, res['name'] if res else 'None', res['fSet'] if res else 'None'))

res = uniFScale.GetLevelByName('Hig', exactMatching=False)
print("GetLevelByName('Hig', {}) = {}, {}".format(uniFScale.name, res['name'] if res else 'None', res['fSet'] if res else 'None'))

res = uniFScale.GetLevelByName('LOw', exactMatching=False)
print("GetLevelByName('LOw', {}) = {}, {}".format(uniFScale.name, res['name'] if res else 'None', res['fSet'] if res else 'None'))

res = uniFScale.GetLevelByName('eD', exactMatching=False)
print("GetLevelByName('eD', {}) = {}, {}".format(uniFScale.name, res['name'] if res else 'None', res['fSet'] if res else 'None'))

res = uniFScale.GetLevelByName('Highest', exactMatching=False)
print("GetLevelByName('Highest', {}) = {}, {}".format(uniFScale.name, res['name'] if res else 'None', res['fSet'] if res else 'None'))

Output:

Finding level by name without exact matching:
GetLevelByName('mIn', FuzzyScale) = Min, Min = <Hyperbolic(x, {"a": 8, "b": 20, "c": 0}), [0.0, 0.23]>
GetLevelByName('max', FuzzyScale) = Max, Max = <Parabolic(x, {"a": 0.77, "b": 0.95}), [0.77, 1.0]>
GetLevelByName('Hig', FuzzyScale) = None, None
GetLevelByName('LOw', FuzzyScale) = Low, Low = <Bell(x, {"a": 0.17, "b": 0.23, "c": 0.34}), [0.17, 0.4]>
GetLevelByName('eD', FuzzyScale) = None, None
GetLevelByName('Highest', FuzzyScale) = None, None

Work with fuzzy logic operators

Checks that number is in [0, 1]:

print('IsCorrectFuzzyNumberValue(0.5) =', IsCorrectFuzzyNumberValue(0.5))
print('IsCorrectFuzzyNumberValue(1.1) =', IsCorrectFuzzyNumberValue(1.1))

Output:

IsCorrectFuzzyNumberValue(0.5) = True
IsCorrectFuzzyNumberValue(1.1) = False

Calculates result of fuzzy NOT, fuzzy NOT with alpha parameter and parabolic fuzzy NOT operations:

print('FNOT(0.25) =', FuzzyNOT(0.25))
print('FNOT(0.25, alpha=0.25) =', FuzzyNOT(0.25, alpha=0.25))
print('FNOT(0.25, alpha=0.75) =', FuzzyNOT(0.25, alpha=0.75))
print('FNOT(0.25, alpha=1) =', FuzzyNOT(0.25, alpha=1))

print('FNOTParabolic(0.25, alpha=0.25) =', FuzzyNOTParabolic(0.25, alpha=0.25))
print('FNOTParabolic(0.25, alpha=0.75) =', FuzzyNOTParabolic(0.25, alpha=0.75))

Output:

FNOT(0.25) = 0.75
FNOT(0.25, alpha=0.25) = 0.25
FNOT(0.25, alpha=0.75) = 0.9166666666666666
FNOT(0.25, alpha=1) = 1.0
FNOTParabolic(0.25, alpha=0.25) = 0.25000000000000017
FNOTParabolic(0.25, alpha=0.75) = 0.9820000000000008

Calculates result of fuzzy AND/OR operations:

print('FuzzyAND(0.25, 0.5) =', FuzzyAND(0.25, 0.5))
print('FuzzyOR(0.25, 0.5) =', FuzzyOR(0.25, 0.5))

Output:

FuzzyAND(0.25, 0.5) = 0.25
FuzzyOR(0.25, 0.5) = 0.5

Calculates result of T-Norm operations, where T-Norm is one of conjunctive operators - logic, algebraic, boundary, drastic:

print("TNorm(0.25, 0.5, 'logic') =", TNorm(0.25, 0.5, normType='logic'))
print("TNorm(0.25, 0.5, 'algebraic') =", TNorm(0.25, 0.5, normType='algebraic'))
print("TNorm(0.25, 0.5, 'boundary') =", TNorm(0.25, 0.5, normType='boundary'))
print("TNorm(0.25, 0.5, 'drastic') =", TNorm(0.25, 0.5, normType='drastic'))

Output:

TNorm(0.25, 0.5, 'logic') = 0.25
TNorm(0.25, 0.5, 'algebraic') = 0.125
TNorm(0.25, 0.5, 'boundary') = 0
TNorm(0.25, 0.5, 'drastic') = 0

Calculates result of S-coNorm operations, where S-coNorm is one of disjunctive operators - logic, algebraic, boundary, drastic:

print("SCoNorm(0.25, 0.5, 'logic') =", SCoNorm(0.25, 0.5, normType='logic'))
print("SCoNorm(0.25, 0.5, 'algebraic') =", SCoNorm(0.25, 0.5, normType='algebraic'))
print("SCoNorm(0.25, 0.5, 'boundary') =", SCoNorm(0.25, 0.5, normType='boundary'))
print("SCoNorm(0.25, 0.5, 'drastic') =", SCoNorm(0.25, 0.5, normType='drastic'))

Output:

SCoNorm(0.25, 0.5, 'logic') = 0.5
SCoNorm(0.25, 0.5, 'algebraic') = 0.625
SCoNorm(0.25, 0.5, 'boundary') = 0.75
SCoNorm(0.25, 0.5, 'drastic') = 1

Calculates result of T-Norm operations for N numbers, N > 2:

print("TNormCompose(0.25, 0.5, 0.75, 'logic') =", TNormCompose(0.25, 0.5, 0.75, normType='logic'))
print("TNormCompose(0.25, 0.5, 0.75, 'algebraic') =", TNormCompose(0.25, 0.5, 0.75, normType='algebraic'))
print("TNormCompose(0.25, 0.5, 0.75, 'boundary') =", TNormCompose(0.25, 0.5, 0.75, normType='boundary'))
print("TNormCompose(0.25, 0.5, 0.75, 'drastic') =", TNormCompose(0.25, 0.5, 0.75, normType='drastic'))

Output:

TNormCompose(0.25, 0.5, 0.75, 'logic') = 0.25
TNormCompose(0.25, 0.5, 0.75, 'algebraic') = 0.09375
TNormCompose(0.25, 0.5, 0.75, 'boundary') = 0
TNormCompose(0.25, 0.5, 0.75, 'drastic') = 0

Calculates result of S-coNorm operations for N numbers, N > 2:

print("SCoNormCompose(0.25, 0.5, 0.75, 'logic') =", SCoNormCompose(0.25, 0.5, 0.75, normType='logic'))
print("SCoNormCompose(0.25, 0.5, 0.75, 'algebraic') =", SCoNormCompose(0.25, 0.5, 0.75, normType='algebraic'))
print("SCoNormCompose(0.25, 0.5, 0.75, 'boundary') =", SCoNormCompose(0.25, 0.5, 0.75, normType='boundary'))
print("SCoNormCompose(0.25, 0.5, 0.75, 'drastic') =", SCoNormCompose(0.25, 0.5, 0.75, normType='drastic'))

Output:

SCoNormCompose(0.25, 0.5, 0.75, 'logic') = 0.75
SCoNormCompose(0.25, 0.5, 0.75, 'algebraic') = 0.90625
SCoNormCompose(0.25, 0.5, 0.75, 'boundary') = 1
SCoNormCompose(0.25, 0.5, 0.75, 'drastic') = 1

Working with other methods

Converting some strings to range of sorted unique numbers with DiapasonParser():

print("Converting some strings to range of sorted unique numbers:")
print('String "1,5" converted to:', DiapasonParser("1,5"))
print('String "1-5" converted to:', DiapasonParser("1-5"))
print('String "8-10, 1-5, 6" converted to:', DiapasonParser("8-10, 1-5, 6"))
print('String "11, 11, 12, 12, 1-5, 3-7" converted to:', DiapasonParser("11, 12, 1-5, 3-7"))

Output:

Converting some strings to range of sorted unique numbers:
String "1,5" converted to: [1, 5]
String "1-5" converted to: [1, 2, 3, 4, 5]
String "8-10, 1-5, 6" converted to: [1, 2, 3, 4, 5, 6, 8, 9, 10]
String "11, 11, 12, 12, 1-5, 3-7" converted to: [1, 2, 3, 4, 5, 6, 7, 11, 12]