Full Blog TOC

Full Blog Table Of Content with Keywords Available HERE

Wednesday, August 7, 2024


 

This post includes examples for basic usage of PyTorch.

For the learning adaline, the input was downloaded from here.


import matplotlib.pyplot as plt
import numpy as np
import torch


def basic_usage():
print('=== basic usage ===')
print('torch version', torch.__version__)
t1 = torch.tensor([[1., 2., 3.], [4., 5., 6.]], dtype=torch.float32)
print('tensor data type', t1.dtype)
print('tensor #dimensions', t1.ndim)
print('tenor shape', t1.shape)
print('tensor data', t1)
print('convert type', t1.to(torch.int))

print('broadcasting')
print(t1 + 100)
print(t1 + torch.tensor([100, 200, 300]))

t2 = torch.tensor([[1., 1.], [1., 1.], [1., 1.]])
print('tensor multiplication', t1 @ t2)

print('convert to numpy - for work with matplotlib')
print(t1.numpy())

print('reshaping')
t3 = torch.arange(6)
print(t3)
print(t3.view(2, 3))
print('column vector', t3.view(-1, 1))


def gpu_selection():
print('=== check GPU ===')
# can run the CLI `nvidia-smi` to show GPU (if exists)
available = torch.cuda.is_available()
print('GPU available?', available)
if available:
torch_device = torch.device('cuda:0')
else:
torch_device = torch.device('cpu')
t1 = torch.tensor([1, 2, 3], dtype=torch.float32, device=torch_device)


def adaline_network():
data = np.genfromtxt('input/input.csv', delimiter=',')
data = torch.tensor(data, dtype=torch.float32)
x = data[:, :-1]
y = data[:, -2:-1]
number_of_samples = x.shape[0]
number_of_features = x.shape[1]
train_factor = 0.7
train_size = int(train_factor * number_of_samples)
x_train, x_test = x[:train_size], x[train_size:]
y_train, y_test = y[:train_size], y[train_size:]

random_seed = 42
learning_rate = 0.01
number_of_epochs = 10
batch_size = 64
torch.manual_seed(random_seed)
linear = torch.nn.Linear(in_features=number_of_features, out_features=1)
optimizer = torch.optim.SGD(linear.parameters(), lr=learning_rate)

training_mean_squared_errors_per_epoch = []
test_mean_squared_errors_per_epoch = []
for epoch_number in range(number_of_epochs):
number_of_train_samples = x_train.shape[0]
shuffled_indices = torch.randperm(number_of_train_samples, dtype=torch.int32)
batches_indices = torch.split(shuffled_indices, batch_size)
for batch_indices in batches_indices:
batch_x = x_train[batch_indices]
batch_y = y_train[batch_indices]
training_predictions = linear.forward(batch_x)
training_mean_squared_error = torch.nn.functional.mse_loss(training_predictions, batch_y)
optimizer.zero_grad()
training_mean_squared_error.backward()
optimizer.step()

with torch.no_grad():

training_predictions = linear(x_train)
training_mean_squared_error = torch.nn.functional.mse_loss(training_predictions, y_train).float()
training_mean_squared_errors_per_epoch.append(training_mean_squared_error)

test_predictions = linear(x_test)
test_mean_squared_error = torch.nn.functional.mse_loss(test_predictions, y_test).float()
test_mean_squared_errors_per_epoch.append(test_mean_squared_error)

print('epoch {} mean squared error: train={} test={}',
epoch_number, training_mean_squared_error, test_mean_squared_error)

plt.plot(training_mean_squared_errors_per_epoch)
plt.plot(test_mean_squared_errors_per_epoch)
plt.xlabel('Epoch Number')
plt.ylabel('Mean Squared Error')
plt.legend(['Train', 'Test'])
plt.savefig('output/mean_squared_error_over_epochs.pdf')


basic_usage()
gpu_selection()
adaline_network()


Sunday, August 4, 2024

Implement Perception Using NumPy and Using PyTorch

Image from: https://datascientest.com/en/perceptron-definition-and-use-cases

 


In this post we include a perceptron implementation using NumPy and using PyTorch.


The Main Code

We use input data from this location. The input structure is as follows:


The main code does the following:
  • Reads the data
  • Split to training data and testing data
  • Creates and trains a single perceptron
  • Validates the training results vs. the testing data


def prepare_input(x, y):
samples_number = y.shape[0]
indices = np.arange(samples_number)
random_state = np.random.RandomState(RANDOM_SEED)
random_state.shuffle(indices)
x, y = x[indices], y[indices]
train_factor = 0.7
train_size = int(train_factor * samples_number)
x_train, x_test = x[:train_size], x[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
print('train shape', x_train.shape, y_train.shape)
print('test shape', x_test.shape, y_test.shape)
return x_train, y_train, x_test, y_test


def normalize_input(mean, std, data):
return (data - mean) / std


def plot_xy_by_classes(samples, labels, file_name, added_line=None):
class1_indices = labels == 0
class2_indices = labels == 1
class1_x = samples[class1_indices, 0]
class1_y = samples[class1_indices, 1]
class2_x = samples[class2_indices, 0]
class2_y = samples[class2_indices, 1]
plt.clf()

if added_line:
line_x, line_y = added_line
plt.plot(line_x, line_y)
plt.scatter(class1_x, class1_y, label='class1', marker='o')
plt.scatter(class2_x, class2_y, label='class2', marker='s')
plt.legend()
plt.savefig(f'output/{file_name}.pdf')


def main():
x, y = read_input()
plot_xy_by_classes(x, y, 'original')
x_train, y_train, x_test, y_test = prepare_input(x, y)
mean = x_train.mean(axis=0)
std = x_train.std(axis=0)
x_train = normalize_input(mean, std, x_train)
x_test = normalize_input(mean, std, x_test)
plot_xy_by_classes(x_train, y_train, 'train')
plot_xy_by_classes(x_test, y_test, 'test')

perceptron = Perceptron(2)
perceptron.train(x_train, y_train, 10)
perceptron.describe()
accuracy = perceptron.evaluate(x_test, y_test)
print('accuracy', accuracy)
plot_xy_by_classes(x_train, y_train, 'train_with_model', added_line=perceptron.model_line())
plot_xy_by_classes(x_test, y_test, 'test_with_model', added_line=perceptron.model_line())


main()

Perceptron Using NumPy


NumPy requires the following dependecies:

pip3 install numpy 
pip3 install mathplotlib


And the Perceptron code is:



class Perceptron:
def __init__(self, features_number):
self.features_number = features_number
self.weights_vector = np.zeros((features_number, 1), dtype=np.float32)
self.bias = np.zeros(1, dtype=np.float32)

def forward(self, x):
z = x @ self.weights_vector + self.bias
predictions = np.where(z > 0., 1, 0)
return predictions

def backward(self, x, y):
predictions = self.forward(x)
errors = y - predictions
return errors

def train(self, x, y, epochs_number):
for epoch_index in range(epochs_number):
for sample_index in range(y.shape[0]):
sample_x = x[sample_index]
sample_label = y[sample_index]
errors = self.backward(sample_x, sample_label)
correction = errors * sample_x
correction = correction.reshape(self.features_number, 1)
self.weights_vector += correction
self.bias += errors

def evaluate(self, x, y):
predictions = self.forward(x).flatten()
correct = predictions == y
accuracy = np.sum(correct) / y.shape[0]
return accuracy

def describe(self):
print('weights', self.weights_vector.flatten())
print('bias', self.bias)

def model_line(self):
x1 = -2
x2 = 2
y1 = -(self.bias + x1 * self.weights_vector[0]) / self.weights_vector[1]
y2 = -(self.bias + x2 * self.weights_vector[0]) / self.weights_vector[1]
line_x = [x1, x2]
line_y = [y1, y2]
return line_x, line_y

Perceptron Using PyTorch


PyTorch requires the following dependencies:

pip3 install numpy
pip3 install mathplotlib
pip3 install torch

And the Perceptron code is:

class Perceptron:
def __init__(self, features_number):
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
self.features_number = features_number
self.weights_vector = torch.zeros(features_number, 1, dtype=torch.float32, device=self.device)
self.bias = torch.zeros(1, dtype=torch.float32, device=self.device)
self.ones = torch.ones(1)
self.zeros = torch.zeros(1)

def forward(self, x):
z = x @ self.weights_vector + self.bias
predictions = torch.where(z > 0., self.ones, self.zeros)
return predictions

def backward(self, x, y):
predictions = self.forward(x)
errors = y - predictions
return errors

def train(self, x, y, epochs_number):
x = torch.tensor(x, dtype=torch.float32, device=self.device)
y = torch.tensor(y, dtype=torch.float32, device=self.device)
for epoch_index in range(epochs_number):
for sample_index in range(y.shape[0]):
sample_x = x[sample_index]
sample_label = y[sample_index]
errors = self.backward(sample_x, sample_label)
correction = errors * sample_x
correction = correction.reshape(self.features_number, 1)
self.weights_vector += correction
self.bias += errors

def evaluate(self, x, y):
x = torch.tensor(x, dtype=torch.float32, device=self.device)
y = torch.tensor(y, dtype=torch.float32, device=self.device)
predictions = self.forward(x).flatten()
correct = predictions == y
accuracy = torch.sum(correct).float() / y.shape[0]
return accuracy

def describe(self):
print('weights', self.weights_vector.flatten())
print('bias', self.bias)

def model_line(self):
x1 = -2
x2 = 2
y1 = -(self.bias + x1 * self.weights_vector[0]) / self.weights_vector[1]
y2 = -(self.bias + x2 * self.weights_vector[0]) / self.weights_vector[1]
line_x = [x1, x2]
line_y = [y1, y2]
return line_x, line_y


Run Results

Plotting the reuslts, we can see the model in the training data:




And the model over the test data:






Thursday, August 1, 2024

Matplotlib Quick Start


 


This is a quickstart for matplotlib.

Many more examples can be located in: 

  • https://matplotlib.org/stable/gallery/index.html
  • https://matplotlib.org/stable/tutorials/index.html


To run the following first install:

pip3 install numpy
pip3 install matplotlib


Examples:


import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
plt.plot(x, np.sin(x))
plt.savefig("plots/line.pdf")

plt.clf()
plt.xlim([3, 6])
plt.ylim([-1, 0.2])
plt.xlabel("x")
plt.ylabel("sin(x)")
plt.plot(x, np.sin(x))
plt.savefig("plots/line_zoom.pdf")

plt.clf()
plt.gca().relim()
plt.gca().autoscale()
plt.plot(x, np.sin(x), linestyle='', marker='x')
plt.savefig("plots/dots.pdf")

plt.clf()
x = np.random.normal(loc=0.0, scale=1.0, size=200)
y = np.random.normal(loc=0.0, scale=1.0, size=200)
plt.scatter(x, y)
plt.savefig("plots/scatter.pdf")

plt.clf()
x = np.random.normal(loc=0.0, scale=1.0, size=10000)
bins = np.arange(-5, 5, 0.1)
plt.hist(x, bins=bins, alpha=0.5)
plt.savefig("plots/histogram.pdf")

plt.clf()
fig, ax = plt.subplots(nrows=2, ncols=3, sharex=True, sharey=True)
x = np.random.normal(loc=0.0, scale=1.0, size=1000)
for row in ax:
for col in row:
x = np.random.normal(loc=0.0, scale=1.0, size=1000)
bins = np.arange(-5, 5, 0.5)
col.hist(x, bins=bins, alpha=0.5)
plt.savefig("plots/subplots.pdf")


Results:















NumPy Cheatsheet with Examples



 

This includes a cheatsheet-like code with examples for numpy.

To make this work, first install numpy:

pip3 install numpy


Examples:

import numpy as np


def multiply_by_scalars():
print("=== multiply by scalars ===")
x = np.array([1., 2., 3.])
weight = np.array([4., 5., 6.])

print("x", x)
print("weight", weight)

# same as: output = x.dot(weight)
output = x @ weight
print(output)


def multiply_matrix():
print("=== multiply matrix ===")
x = np.ones(([2, 3]), dtype=int)
y = np.ones(([3, 4]), dtype=int)
print(x @ y)

print("transpose to multiply")
print(x @ x.transpose())


def multiple_dimensions_array():
print("=== multiple dimensions array ===")
array_2d = np.array([[1, 2, 3], [4, 5, 6]])
print("2D array", array_2d)
print("index access", array_2d[1, 2])


def convert_types():
print("=== convert types ===")
a = np.array([1, 2, 3])

print("before convert")
print("a", a)
print("a type", a.dtype)

print("after convert (better to work with float32 on GPU)")
a = a.astype(np.float32)
print("a", a)
print("a type", a.dtype)


def check_dimensions():
print("=== check dimensions ===")
array_3d = np.array([
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
],
[
[1001, 1002, 1003, 1004],
[1005, 1006, 1007, 1008],
[1009, 10010, 10011, 10012],
],
])
print(array_3d)
print("dimensions", array_3d.ndim)
print("shape", array_3d.shape)


def create_predefined_arrays():
print("=== create predefined arrays ===")

print("ones")
print(np.ones((2, 3), dtype=np.int32))

print("zeros")
print(np.zeros((2, 3), dtype=np.int32))

print("constants")
print(42 + np.zeros((2, 3), dtype=np.int32))

print("I matrix")
print(np.eye(4, dtype=np.int32))

print("diagonal")
print(np.diag((5, 6, 7)))

print("configure range and step size")
print(np.arange(10., 5., -0.5))

print("configure number of elements within range")
print(np.linspace(0., 20., num=4))

print("random arrays")
np.random.seed(42)
print(np.random.rand(3, 2))


def array_slices():
print("=== array slices ===")
a = np.array([[1, 2, 3], [4, 5, 6]])

print("slice by index")
print(a[0])

print("slice from end")
print(a[-1])

print("slice by range")
print(a[1, 0:2])

print("slice by column")
print(a[:, 0])


def array_math():
print("=== array math ===")
a = np.array([[1, 2, 3], [4, 5, 6]])

print(a)

print("add")
print(a + 1)

print("power")
print(a ** 2)

print("sum by rows")
print(np.add.reduce(a, axis=0))

print("sum by columns")
print(np.add.reduce(a, axis=1))

print("max by columns")
print(np.max(a, axis=1))


def broadcast():
print("=== broadcast ===")
a = np.array([[1, 1, 1], [7, 7, 7]])
b = np.array([1, 2, 3])
print(a + b)


def slice_changes_original():
print("=== slice changes original ===")
a = np.array([[1, 1, 1], [2, 2, 2], [3, 3, 3]])

print("default slice is a view on the original")
first_row = a[0]
first_row += 1
print(a)

print("using copy() duplicates memory")
second_row = a[1].copy()
second_row += 1
print(a)

print("fancy index (non continuous) always uses a copy")
fancy_slice = a[[0, 2]]
fancy_slice += 100
print(fancy_slice)
print(a)


def boolean_masks():
print("=== boolean masks ===")
a = np.array([[1, 5, 2], [8, 4, 7]])
print(a >= 5)

print("multiple conditions")
complex_condition = (a >= 5) & (a < 8)
print(complex_condition)

print("select only the elements that match a condition (this is fancy condition)")
print(a[complex_condition])

print("check how many elements answer a condition")
print(complex_condition.sum())

print("update by boolean condition")
print(np.where(a >= 5, 100, 55))


def reshape_array():
print("=== reshape array ===")
a = np.random.rand(12)
print(a)

array_2d = a.reshape(3, 4)
print(array_2d)

print("reshape shares the memory of the original")
print(np.may_share_memory(a, array_2d))

print("reshape without knowing one dimension")
print(a.reshape(-1, 6))

print("flatten array")
i = np.eye(3)
# same as: print(i.reshape(-1))
print(i.flatten())

print("create vector")
a = np.array([1, 2, 3])
# same as: print(a.reshape(-1, 1))
# same as: print(a[:, None])
print(a[:, np.newaxis])


def concatenate_array():
print("=== concatenate array ===")
a = np.array([[1, 2, 3], [4, 5, 6]])

print("add rows")
print(np.concatenate((a, a), axis=0))

print("add columns")
print(np.concatenate((a, a), axis=1))


multiply_by_scalars()
multiply_matrix()
multiple_dimensions_array()
convert_types()
check_dimensions()
create_predefined_arrays()
array_slices()
array_math()
broadcast()
slice_changes_original()
boolean_masks()
reshape_array()
concatenate_array()



Sunday, July 21, 2024

Using NATS Key-Value from GO




In this post we will review the steps to use NATS key-value from a GO application.


Install NATS

The NATS key-value is a feature supplied by JetStream, hence we install NATS with JetStream enabled:

Create a config file:

config:
cluster:
enabled: true
replicas: 3
jetstream:
enabled: true


And install using helm:

helm repo add nats https://nats-io.github.io/k8s/helm/charts/
helm install nats nats/nats -f config.yaml
#check connectivity
kubectl exec -it deployment/nats-box -- nats pub test hi


Using Key-Value from GO


The following includes a key-value access with the following:

  1. Put, Get, and Delete operations
  2. Watcher to get notifications for any update of the key
  3. Parallel updates and blocking awareness


package main

import (
"context"
"fmt"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"time"
)

func main() {

natsConnection, err := nats.Connect("nats://nats:4222")
if err != nil {
panic(err)
}

jetStreamConnection, err := jetstream.New(natsConnection)
if err != nil {
panic(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

keyValue, err := jetStreamConnection.CreateKeyValue(ctx, jetstream.KeyValueConfig{
Bucket: "myKeyValue",
})
if err != nil {
panic(err)
}

const key = "myKey"

// run a watcher in the background to get updates on a specific key
watcher, _ := keyValue.Watch(ctx, key)
defer watcher.Stop()
go func() {
for update := range watcher.Updates() {
if update == nil {
fmt.Printf("watcer empty event\n")
} else {
update.Operation()
fmt.Printf("watcer operation %v key %s revision %d -> value %q\n",
update.Operation().String(), update.Key(), update.Revision(), string(update.Value()))
}
}
}()

// update #1
sequence, err := keyValue.Put(ctx, key, []byte("value1"))
if err != nil {
panic(err)
}
fmt.Printf("the update sequence is %v\n", sequence)

entry, err := keyValue.Get(ctx, key)
if err != nil {
panic(err)
}
fmt.Printf("key %s revision %d -> value %q\n", entry.Key(), entry.Revision(), string(entry.Value()))

// update #2
sequence, err = keyValue.Put(ctx, key, []byte("value2"))
if err != nil {
panic(err)
}
fmt.Printf("the update sequence is %v\n", sequence)
entry, err = keyValue.Get(ctx, key)
if err != nil {
panic(err)
}
fmt.Printf("key %s revision %d -> value %q\n", entry.Key(), entry.Revision(), string(entry.Value()))

// here we see that we can block parallel updates by specifying the revision we expect to update
sequence, err = keyValue.Update(ctx, key, []byte("parallelValue"), 1)
fmt.Printf("expected error: %s\n", err)

// ony when providing the correct revision, we are allowed to update
sequence, err = keyValue.Update(ctx, key, []byte("parallelValue"), entry.Revision())
if err != nil {
panic(err)
}

err = keyValue.Delete(ctx, key)
if err != nil {
panic(err)
}

// wait for watcher actions
time.Sleep(5 * time.Second)
}


And the output is:

watcer empty event
the update sequence is 1
watcer operation KeyValuePutOp key myKey revision 1 -> value "value1"
key myKey revision 1 -> value "value1"
the update sequence is 2
watcer operation KeyValuePutOp key myKey revision 2 -> value "value2"
key myKey revision 2 -> value "value2"
expected error: nats: nats: API error: code=400 err_code=10071 description=wrong last sequence: 2
watcer operation KeyValuePutOp key myKey revision 3 -> value "parallelValue"
watcer operation KeyValueDeleteOp key myKey revision 4 -> value ""



 


Sunday, July 14, 2024

Embedding NATS server in a GO application

 

In this post we examine two methods of embedding a NATS server as part of a GO application. This post is base on this video.

First we we use a NATS server that includes a listener so we can have NATS connections from the external world, and then we run NATS server without a listener.


Embedded NATS server with a Listener

This can be used when we want the application to run NATS server embedded within the GO process, and we still want to preserve the ability to get messages from external clients through the network. We can even run this NATS servers as part of a cluster.


package main

import (
"errors"
"fmt"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"time"
)

func main() {
options := server.Options{}
natsServer, err := server.NewServer(&options)
if err != nil {
panic(err)
}

natsServer.ConfigureLogger()

go natsServer.Start()

if !natsServer.ReadyForConnections(time.Minute) {
panic(errors.New("nats server not ready"))
}

natsConnection, err := nats.Connect(natsServer.ClientURL())
if err != nil {
panic(err)
}

natsSubscription, err := natsConnection.Subscribe("queue1", func(message *nats.Msg) {
response := fmt.Sprintf("got your message: %s", string(message.Data))
err := message.Respond([]byte(response))
if err != nil {
panic(err)
}
})

if err != nil {
panic(err)
}

for i := range 10 {
message := fmt.Sprintf("My %v message", i)
response, err := natsConnection.Request("queue1", []byte(message), time.Second)
if err != nil {
panic(err)
}
fmt.Printf("got response: %v\n", string(response.Data))
}

err = natsSubscription.Unsubscribe()
if err != nil {
panic(err)
}

natsServer.WaitForShutdown()
}

And the output is:

[19234] [INF] Starting nats-server
[19234] [INF] Version: 2.10.17
[19234] [INF] Git: [not set]
[19234] [INF] Name: NCD4DRH4CVNVY44VGEPOTKKJMGM524KCUN26GQNQTXYYKCGNEXUNIZ6P
[19234] [INF] ID: NCD4DRH4CVNVY44VGEPOTKKJMGM524KCUN26GQNQTXYYKCGNEXUNIZ6P
[19234] [INF] Listening for client connections on 0.0.0.0:4222
[19234] [INF] Server is ready
got response: got your message: My 0 message
got response: got your message: My 1 message
got response: got your message: My 2 message
got response: got your message: My 3 message
got response: got your message: My 4 message
got response: got your message: My 5 message
got response: got your message: My 6 message
got response: got your message: My 7 message
got response: got your message: My 8 message
got response: got your message: My 9 message
^C[19234] [INF] Initiating Shutdown...
[19234] [INF] Server Exiting..




Embedded NATS server with NO Listener

This can be used for an application that requires NATS only for its internal message distribution, and separation between internal modules. Using in-process API instead of localhost communication is much faster.

To do this, we configure the NATS server to "DontListen", and use a in-process option for the client connection.


package main

import (
"errors"
"fmt"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"time"
)

func main() {
serverOptions := server.Options{
DontListen: true,
}
natsServer, err := server.NewServer(&serverOptions)
if err != nil {
panic(err)
}

natsServer.ConfigureLogger()

go natsServer.Start()

if !natsServer.ReadyForConnections(time.Minute) {
panic(errors.New("nats server not ready"))
}

option := nats.InProcessServer(natsServer)
natsConnection, err := nats.Connect(natsServer.ClientURL(), option)
if err != nil {
panic(err)
}

natsSubscription, err := natsConnection.Subscribe("queue1", func(message *nats.Msg) {
response := fmt.Sprintf("got your message: %s", string(message.Data))
err := message.Respond([]byte(response))
if err != nil {
panic(err)
}
})

if err != nil {
panic(err)
}

for i := range 10 {
message := fmt.Sprintf("My %v message", i)
response, err := natsConnection.Request("queue1", []byte(message), time.Second)
if err != nil {
panic(err)
}
fmt.Printf("got response: %v\n", string(response.Data))
}

err = natsSubscription.Unsubscribe()
if err != nil {
panic(err)
}

natsServer.WaitForShutdown()
}


and the output is:

[20802] [INF] Starting nats-server
[20802] [INF] Version: 2.10.17
[20802] [INF] Git: [not set]
[20802] [INF] Name: NB2IB6QLEWN2XNPJUAVIWKRICFJUIQJ2CRQWGOC2XKV7IFO36HOK7WXM
[20802] [INF] ID: NB2IB6QLEWN2XNPJUAVIWKRICFJUIQJ2CRQWGOC2XKV7IFO36HOK7WXM
[20802] [INF] Server is ready
got response: got your message: My 0 message
got response: got your message: My 1 message
got response: got your message: My 2 message
got response: got your message: My 3 message
got response: got your message: My 4 message
got response: got your message: My 5 message
got response: got your message: My 6 message
got response: got your message: My 7 message
got response: got your message: My 8 message
got response: got your message: My 9 message
^C[20802] [INF] Initiating Shutdown...
[20802] [INF] Server Exiting..



Monday, June 24, 2024

Camel Case Words Count

 

I've recently had to analyze URL path elements, and check each word in it. However I ran into an issue that I need to check each camel case word. For this, I've create a function to split a string to camel case words.



import (
"fmt"
"unicode"
)

func SplitCamelWords(segment string) []string {
var words []string
var word string

var prevCharLetter bool
var prevCharUpper bool
inUpperWord := false
for charIndex, currentChar := range segment {

currCharUpper := unicode.IsUpper(currentChar)
currCharLetter := unicode.IsLetter(currentChar)

if charIndex > 0 {

if currCharLetter {
if prevCharLetter {
if prevCharUpper {
if currCharUpper {
inUpperWord = true
} else {
if inUpperWord {
words = append(words, word)
word = ""
}
}
} else {
if currCharUpper {
words = append(words, word)
word = ""
} else {
inUpperWord = false
}
}
}

} else {
inUpperWord = false
if prevCharLetter {
words = append(words, word)
word = ""
}
}
}

prevCharUpper = currCharUpper
prevCharLetter = currCharLetter
word += fmt.Sprintf("%c", currentChar)
}

if prevCharLetter {
words = append(words, word)
word = ""
}

return words
}



and a test output is:

A -> [A]
a -> [a]
Aaaaa -> [Aaaaa]
AAAAA -> [AAAAA]
aaaaa -> [aaaaa]
A1 -> [A]
a1 -> [a]
Aaaaa1 -> [Aaaaa]
AAAAA1 -> [AAAAA]
aaaaa1 -> [aaaaa]
aB -> [a B]
aaaaB -> [aaaa B]
AaaaB -> [Aaaa B]
AaaaBBBB -> [Aaaa BBBB]
AaaaBbbbb -> [Aaaa Bbbbb]
aB2 -> [a B]
aaaaB2 -> [aaaa B]
AaaaB2 -> [Aaaa B]
AaaaBBBB2 -> [Aaaa BBBB]
AaaaBbbbb2 -> [Aaaa Bbbbb]
Aaaa1b -> [Aaaa 1b]
Aaaa1B -> [Aaaa 1B]
Aaaa1bbb -> [Aaaa 1bbb]
Aaaa1Bbb -> [Aaaa 1Bbb]
AaBbCc -> [Aa Bb Cc]
A1B2C3 -> [A 1B 2C]
Aa1Bb2Cc3 -> [Aa 1Bb 2Cc]
AAA BBB Ccc -> [AAA BBB Ccc]
AAA1BBB Ccc -> [AAA 1BBB Ccc]
AAA1BBB Ccc -> [AAA 1BBB Ccc]



Notice that the task is not as obvious as it might appear at first glace. We cannot just split whenever we find an upper case character, but instead we need to consider the sequence of characters.

For example: HouseOfLove would count as 3 words: House, Of, Love.

However, houseOFLove would still count as 3 words, since we have a sequence of upper case characters: House, OF, Love.