import numpy as np

def pause():
    print('------------------------'); input()    # for demo purposes


# Not lists but arrays, which reflects classic (C/C++) data organization.


### Integers are not "infinite"!
a = np.array( [10, 50, 100] )
print( a )
print( a+10, a*2, a**6 )   
'''
[ 20  60 110] [ 20 100 200] [    1000000 -1554869184  -727379968]
'''
### note the regular spacing in the output:
# [ 20  60 110] [ 20 100 200] [    1000000 -1554869184  -727379968]
#  xxx xxx xxx   xxx xxx xxx   12345678901 12345678901 12345678901

# all items in the list are printed with the same length,
# including additional spaces.

# dtype stands for "Data Type"
print( a.dtype.name, a.dtype.itemsize )
# output int32 4
# the size is given in bytes,
# 32 in the int name stands for 32 bits, which is 4x8bits in a byte.

# ---------------------------------------------------------------------------
pause()

a = np.array( [10.0, 50.0, 100.0] )
print( a )
print( a.dtype.name, a.dtype.itemsize )

'''
float64 8
'''
# the size is given in bytes,
# 64 in the int name stands for 64 bits, which is 8x8bits in a byte.
print( a+10, a*2, a**6 )
'''
# [ 20.  60. 110.] [ 20. 100. 200.] [1.0000e+06 1.5625e+10 1.0000e+12]
'''

###  note the defaults:
# - Decimal point informs about being a decimal (float) number, not integer
# - Sufficiently big numbers are presented in scientific format.
# - The number of decimal places displayed. (Can be regulated, of course.)

# ---------------------------------------------------------------------------
pause()


# More limitations:

# Define the type of data stored in the array
# BEFORE doing anything with the array
# It reflects more or less usual approach in C/C++/C#/Java...

# The size of array cannot be changed!
# Define the size of the array based on thorough
# analysis of the given problem!


# ---------------------------------------------------------------------------


# Create array

### based on some usual python array
# if it has more dimensions, the array must be a rectangle or box shaped:
a1 = np.array( [1, 2, 3,  4] )
a1f = np.array( [1, 2, 3, 4.1] )   # because of 4.1, all items become floats

print( a1 )
print( a1f,  a1f.dtype.name, a1f.dtype.itemsize  )
'''
[1 2 3 4]
[1.  2.  3.  4.1] float64 8
'''

# ---------------------------------------------------------------------------
pause()

# Standard initializations -- zeros

a20 = np.zeros( (3, 5) )
# note the dimensions of the array are a tuple, thus additional brackets.
print( a20 )
print( a20.dtype.name, a20.dtype.itemsize )
'''
[[0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]
float64 8
'''
# Because numpy tends to scientific computing,
# by default the type of values is float,
# not always perfect for computer science.
### specify the type of data values:
a20 = np.zeros( (3, 5), dtype = np.int64 )
print( a20 )
print( a20.dtype.name, a20.dtype.itemsize )
'''
[[0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]]
int64 8
'''
### note the table-perception-friendly format of the output -
# clear rows and columns of the same width


# ---------------------------------------------------------------------------
pause()

# array homogenity

# A.
# Assigning value of different type to some array field
# generates an error if the value cannot be interpreted as number
# a21[1][1] = '3.14'
print( a20 )
'''
    a21[1][1] = '3.14'
    ~~~~~~^^^
ValueError: invalid literal for int() with base 10: '3.14'
'''

# B.
# Assigning value of different type to some array field
# may change the original value when the value is a number:
a20[1][1] = -19.99
print( a20 )
'''
[[ 0  0  0  0  0]
 [ 0 19  0  0  0]
 [ 0  0  0  0  0]]
 '''
# The value is rounded down to 19, not to closer 20.
# Btw, negative value -19.99 results in -19


# ---------------------------------------------------------------------------
pause()

# Standard initializations -- ones
a21 = np.ones( (3,5), dtype = 'int64' )
print( a21 )
print( a21.dtype.name, a21.dtype.itemsize )
'''
[[1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]]
int64 8
'''

# ---------------------------------------------------------------------------
pause()


# range
# no **range** in numpy, but **arange** instead
a30 = np.arange( 3, 10 )
print( a30 )
# Why separate function for the same task?
# arange handles floats as well:
a31 = np.arange( 3.0, 5.0, 0.2 )
# last value is the step (increment)
print( a31 )
print( a31.dtype.name, a31.dtype.itemsize )
'''
[3.  3.2 3.4 3.6 3.8 4.  4.2 4.4 4.6 4.8]
float64 8
'''
# instead of increment, one may want to specify
# the number of data points in some interval
# (e.g. divide segment [10, 20] into 5 pieces
a32 = np.linspace( 10, 20, 5 )
print( a32 )
print( a32.dtype.name, a32.dtype.itemsize )
'''
[10.  12.5 15.  17.5 20. ]
float64 8
'''
# Note -
# - automatical change (typecast) of int to float
# - the range is divided into 4 (!!) intervals
# and their endpoints are returned.
# another example:
a33 = np.linspace( 0, 100, 11 )
print( a33 )
'''
[  0.  10.  20.  30.  40.  50.  60.  70.  80.  90. 100.]
'''


################################################################################
# Try it yourself:  roll array or  flip it instead of rotating
# https://numpy.org/doc/stable/reference/generated/numpy.roll.html
# https://numpy.org/doc/stable/reference/generated/numpy.flip.html
# array.reverse() works in plain python





