diff --git a/homework-g595-ferenets/pom.xml b/homework-g595-ferenets/pom.xml new file mode 100644 index 000000000..d85798e58 --- /dev/null +++ b/homework-g595-ferenets/pom.xml @@ -0,0 +1,28 @@ + + + + mipt-java-2016 + ru.mipt.java2016 + 1.0.0 + + 4.0.0 + + homework-g595-ferenets + 1.0.0 + + + ru.mipt.java2016 + homework-base + 1.0.0 + + + ru.mipt.java2016 + homework-tests + 1.0.0 + + + + + \ No newline at end of file diff --git a/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task1/MyCalculator.java b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task1/MyCalculator.java new file mode 100644 index 000000000..e5728f30e --- /dev/null +++ b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task1/MyCalculator.java @@ -0,0 +1,139 @@ +package ru.mipt.java2016.homework.g595.ferenets.task1; + +import ru.mipt.java2016.homework.base.task1.Calculator; +import ru.mipt.java2016.homework.base.task1.ParsingException; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.HashSet; + + +public class MyCalculator implements Calculator { + + private static final HashSet ACCEPTABLE_SYMBOLS = new HashSet<>( + Arrays.asList('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '+', '-', '*', '/', + '(', ')')); + private static final HashSet DIGITS = + new HashSet<>(Arrays.asList('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')); + private static final HashSet OPERATORS = + new HashSet<>(Arrays.asList('+', '-', '*', '/')); + private static final HashSet SPACES = new HashSet<>(Arrays.asList(' ', '\n', '\t')); + + + @Override + public double calculate(String expression) throws ParsingException { + if (expression == null) { + throw new ParsingException("Null Expression"); + } + int bracketsBalance = 0; + String expressionInBrackets = ""; + String expressionOutOfBrackets = ""; + for (char currentCharacter : expression.toCharArray()) { + if (!ACCEPTABLE_SYMBOLS.contains(currentCharacter) && !SPACES + .contains(currentCharacter)) { + throw new ParsingException("Bad expression"); + } else if (currentCharacter == '(') { + bracketsBalance += 1; + } else { + if (currentCharacter == ')') { + bracketsBalance -= 1; + if (bracketsBalance < 0) { + throw new ParsingException("Bad Brackets Balance"); + } + if (bracketsBalance == 0) { + expressionOutOfBrackets += calculate(expressionInBrackets); + expressionInBrackets = ""; + } + } else { + if (bracketsBalance == 0) { + expressionOutOfBrackets += currentCharacter; + } else { + expressionInBrackets += currentCharacter; + } + } + } + } + if (bracketsBalance != 0) { + throw new ParsingException("Bad Brackets Balance"); + } + ArrayDeque numberArrayDeque = new ArrayDeque<>(); + ArrayDeque operatorArrayDeque = new ArrayDeque<>(); + boolean isReadingNumber = false; + boolean isDotInNumber = false; + boolean wasOperator = true; + String number = ""; + expression = expressionOutOfBrackets; + for (char currentCharacter : expression.toCharArray()) { + if (DIGITS.contains(currentCharacter) || currentCharacter == '.') { + wasOperator = false; + if (!isReadingNumber) { + if (currentCharacter == '.') { + throw new ParsingException("Dot is in the beginning of number"); + } else { + number = "" + currentCharacter; + isDotInNumber = false; + isReadingNumber = true; + } + } else { + if (currentCharacter == '.') { + if (isDotInNumber) { + throw new ParsingException("Two Dots In Number"); + } else { + isDotInNumber = true; + } + } + number += currentCharacter; + } + } else if (OPERATORS.contains(currentCharacter)) { + if (wasOperator) { + if (currentCharacter == '-') { + if (number.equals("-")) { + number = ""; + } else { + number = "-"; + } + isReadingNumber = true; + isDotInNumber = false; + } else { + throw new ParsingException("Two Operators Together"); + } + continue; + } + wasOperator = true; + numberArrayDeque.add(Double.valueOf(number)); + isReadingNumber = false; + operatorArrayDeque.add(currentCharacter); + } + } + if (isReadingNumber) { + numberArrayDeque.add(Double.valueOf(number)); + } + if (numberArrayDeque.isEmpty()) { + throw new ParsingException("Too Few Numbers"); + } + ArrayDeque newNumberArrayDeque = new ArrayDeque<>(); + ArrayDeque newOperatorArrayDeque = new ArrayDeque<>(); + while (numberArrayDeque.size() > 1) { + char currentOperator = operatorArrayDeque.pop(); + if (currentOperator == '+' || currentOperator == '-') { + newNumberArrayDeque.add(numberArrayDeque.pop()); + newOperatorArrayDeque.add(currentOperator); + } else if (currentOperator == '*') { + numberArrayDeque.push(numberArrayDeque.pop() * numberArrayDeque.pop()); + } else if (currentOperator == '/') { + numberArrayDeque.push(numberArrayDeque.pop() / numberArrayDeque.pop()); + } + } + newNumberArrayDeque.add(numberArrayDeque.pop()); + Double result = newNumberArrayDeque.pop(); + while (!newNumberArrayDeque.isEmpty()) { + char currentOperator = newOperatorArrayDeque.pop(); + if (currentOperator == '+') { + result += newNumberArrayDeque.pop(); + } else if (currentOperator == '-') { + result -= newNumberArrayDeque.pop(); + } + } + return result; + } +} diff --git a/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/DoubleSerializationStrategy.java b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/DoubleSerializationStrategy.java new file mode 100644 index 000000000..6fbc143f7 --- /dev/null +++ b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/DoubleSerializationStrategy.java @@ -0,0 +1,17 @@ +package ru.mipt.java2016.homework.g595.ferenets.task2; + +import java.io.IOException; +import java.io.RandomAccessFile; + + +public class DoubleSerializationStrategy implements SerializationStrategy { + @Override + public Double read(RandomAccessFile file) throws IOException { + return file.readDouble(); + } + + @Override + public void write(RandomAccessFile file, Double value) throws IOException { + file.writeDouble(value); + } +} diff --git a/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/IntegerSerializationStrategy.java b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/IntegerSerializationStrategy.java new file mode 100644 index 000000000..52e198acd --- /dev/null +++ b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/IntegerSerializationStrategy.java @@ -0,0 +1,18 @@ +package ru.mipt.java2016.homework.g595.ferenets.task2; + +import java.io.IOException; +import java.io.RandomAccessFile; + + +public class IntegerSerializationStrategy implements SerializationStrategy { + + @Override + public Integer read(RandomAccessFile file) throws IOException { + return file.readInt(); + } + + @Override + public void write(RandomAccessFile file, Integer value) throws IOException { + file.writeInt(value); + } +} diff --git a/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/MyKeyValueStorage.java b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/MyKeyValueStorage.java new file mode 100644 index 000000000..6f64d73bf --- /dev/null +++ b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/MyKeyValueStorage.java @@ -0,0 +1,104 @@ +package ru.mipt.java2016.homework.g595.ferenets.task2; + +import ru.mipt.java2016.homework.base.task2.KeyValueStorage; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.HashMap; +import java.util.Iterator; + +public class MyKeyValueStorage implements KeyValueStorage { + + private HashMap map; + private SerializationStrategy keySerializator; + private SerializationStrategy valueSerializator; + private RandomAccessFile fileRA; + private boolean opened; + + + public MyKeyValueStorage(String path, SerializationStrategy argKeySerializator, + SerializationStrategy argValueSerializator) throws IOException { + map = new HashMap<>(); + keySerializator = argKeySerializator; + valueSerializator = argValueSerializator; + opened = true; + String pathToStorage = path + File.separator + "storage.txt"; + try { + File file = new File(pathToStorage); + if (file.createNewFile()) { + fileRA = new RandomAccessFile(file.getPath(), "rw"); + } else { + fileRA = new RandomAccessFile(file, "rw"); + int elementsCount = fileRA.readInt(); + for (int i = 0; i < elementsCount; ++i) { + K currentKey = this.keySerializator.read(fileRA); + V currentValue = this.valueSerializator.read(fileRA); + map.put(currentKey, currentValue); + } + } + } catch (FileNotFoundException e) { + throw new FileNotFoundException("File is not found"); + } + } + + private void checkFileAccess() { + if (!opened) { + throw new IllegalStateException("The storage is closed"); + } + } + + @Override + public V read(K key) { + checkFileAccess(); + return map.get(key); + } + + @Override + public boolean exists(K key) { + checkFileAccess(); + return map.containsKey(key); + } + + @Override + public void write(K key, V value) { + checkFileAccess(); + map.put(key, value); + } + + @Override + public void delete(K key) { + checkFileAccess(); + map.remove(key); + } + + @Override + public Iterator readKeys() { + checkFileAccess(); + return map.keySet().iterator(); + } + + @Override + public int size() { + checkFileAccess(); + return map.size(); + } + + @Override + public void close() throws IOException { + try { + fileRA.setLength(0); + fileRA.seek(0); + fileRA.writeInt(map.size()); + for (HashMap.Entry entry : map.entrySet()) { + keySerializator.write(fileRA, entry.getKey()); + valueSerializator.write(fileRA, entry.getValue()); + } + fileRA.close(); + opened = false; + } catch (IOException e) { + throw new IOException("Closed."); + } + } +} diff --git a/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/SerializationStrategy.java b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/SerializationStrategy.java new file mode 100644 index 000000000..3d04403e1 --- /dev/null +++ b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/SerializationStrategy.java @@ -0,0 +1,11 @@ +package ru.mipt.java2016.homework.g595.ferenets.task2; + + +import java.io.IOException; +import java.io.RandomAccessFile; + +public interface SerializationStrategy { + T read(RandomAccessFile file) throws IOException; + + void write(RandomAccessFile file, T value) throws IOException; +} diff --git a/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/StringSerializationStrategy.java b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/StringSerializationStrategy.java new file mode 100644 index 000000000..edb203134 --- /dev/null +++ b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/StringSerializationStrategy.java @@ -0,0 +1,17 @@ +package ru.mipt.java2016.homework.g595.ferenets.task2; + +import java.io.IOException; +import java.io.RandomAccessFile; + + +public class StringSerializationStrategy implements SerializationStrategy { + @Override + public String read(RandomAccessFile file) throws IOException { + return file.readUTF(); + } + + @Override + public void write(RandomAccessFile file, String value) throws IOException { + file.writeUTF(value); + } +} diff --git a/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/StudentKeySerializationStrategy.java b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/StudentKeySerializationStrategy.java new file mode 100644 index 000000000..599f78dbf --- /dev/null +++ b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/StudentKeySerializationStrategy.java @@ -0,0 +1,20 @@ +package ru.mipt.java2016.homework.g595.ferenets.task2; + + +import ru.mipt.java2016.homework.tests.task2.StudentKey; + +import java.io.IOException; +import java.io.RandomAccessFile; + +public class StudentKeySerializationStrategy implements SerializationStrategy { + @Override + public StudentKey read(RandomAccessFile file) throws IOException { + return new StudentKey(file.readInt(), file.readUTF()); + } + + @Override + public void write(RandomAccessFile file, StudentKey value) throws IOException { + file.writeInt(value.getGroupId()); + file.writeUTF(value.getName()); + } +} diff --git a/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/StudentSerializationStrategy.java b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/StudentSerializationStrategy.java new file mode 100644 index 000000000..fc2d619ca --- /dev/null +++ b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task2/StudentSerializationStrategy.java @@ -0,0 +1,26 @@ +package ru.mipt.java2016.homework.g595.ferenets.task2; + + +import ru.mipt.java2016.homework.tests.task2.Student; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Date; + +public class StudentSerializationStrategy implements SerializationStrategy { + @Override + public Student read(RandomAccessFile file) throws IOException { + return new Student(file.readInt(), file.readUTF(), file.readUTF(), + new Date(file.readLong()), file.readBoolean(), file.readDouble()); + } + + @Override + public void write(RandomAccessFile file, Student value) throws IOException { + file.writeInt(value.getGroupId()); + file.writeUTF(value.getName()); + file.writeUTF(value.getHometown()); + file.writeLong(value.getBirthDate().getTime()); + file.writeBoolean(value.isHasDormitory()); + file.writeDouble(value.getAverageScore()); + } +} diff --git a/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task3/MyOptimizedStorage.java b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task3/MyOptimizedStorage.java new file mode 100644 index 000000000..b9e5ccfbc --- /dev/null +++ b/homework-g595-ferenets/src/main/java/ru/mipt/java2016/homework/g595/ferenets/task3/MyOptimizedStorage.java @@ -0,0 +1,186 @@ +package ru.mipt.java2016.homework.g595.ferenets.task3; + + +import ru.mipt.java2016.homework.base.task2.KeyValueStorage; +import ru.mipt.java2016.homework.g595.ferenets.task2.SerializationStrategy; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.*; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + + +public class MyOptimizedStorage implements KeyValueStorage { + private static final int MAX_MEM_SIZE = 100; + private SerializationStrategy keySerialization; + private SerializationStrategy valueSerialization; + private HashMap map = new HashMap(); + private TreeMap keyValueOffset = new TreeMap(); + private HashSet keySet = new HashSet(); + private boolean opened; + private RandomAccessFile storage; + private RandomAccessFile offsets; + private File storageFile; + private File offsetsFile; + private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + + + MyOptimizedStorage(String path, SerializationStrategy argKeySerialization, + SerializationStrategy argValueSerialization) throws IOException { + try { + keySerialization = argKeySerialization; + valueSerialization = argValueSerialization; + String storagePath = path + File.separator + "storage.txt"; + String offsetsPath = path + File.separator + "offsets.txt"; + opened = true; + offsetsFile = new File(offsetsPath); + storageFile = new File(storagePath); + if (!offsetsFile.createNewFile()) { + offsets = new RandomAccessFile(offsetsFile, "rw"); + storage = new RandomAccessFile(storageFile, "rw"); + int offsetsSize = offsets.readInt(); + for (int i = 0; i < offsetsSize; i++) { + K key = keySerialization.read(offsets); + long offset = offsets.readLong(); + keyValueOffset.put(key, offset); + keySet.add(key); + } + } else { + offsets = new RandomAccessFile(offsetsFile, "rw"); + storage = new RandomAccessFile(storageFile, "rw"); + } + } catch (IOException e) { + e.printStackTrace(); + } + offsets.close(); + } + + + private void checkFileAccess() { + if (!opened) { + throw new IllegalStateException("The storage is closed"); + } + } + + private void pushMapIntoFile() throws IOException { + readWriteLock.writeLock().lock(); + try { + storage.seek(storage.length()); + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == null) { + continue; + } + keyValueOffset.put(entry.getKey(), storage.getFilePointer()); + valueSerialization.write(storage, entry.getValue()); + } + map.clear(); + } finally { + readWriteLock.writeLock().unlock(); + } + } + + @Override + public V read(K key) { + readWriteLock.writeLock().lock(); + checkFileAccess(); + try { + if (map.containsKey(key)) { + return map.get(key); + } + if (keyValueOffset.containsKey(key)) { + long offset = keyValueOffset.get(key); + storage.seek(offset); + V value = valueSerialization.read(storage); + return value; + } + return null; + } catch (IOException e) { + throw new IllegalStateException("Key is wrong"); + } finally { + readWriteLock.writeLock().unlock(); + } + } + + @Override + public boolean exists(K key) { + readWriteLock.readLock().lock(); + try { + checkFileAccess(); + return keySet.contains(key); + } finally { + readWriteLock.readLock().unlock(); + } + } + + @Override + public void write(K key, V value) { + readWriteLock.writeLock().lock(); + checkFileAccess(); + map.put(key, value); + keySet.add(key); + try { + if (map.size() > MAX_MEM_SIZE) { + pushMapIntoFile(); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + readWriteLock.writeLock().unlock(); + } + } + + @Override + public void delete(K key) { + readWriteLock.readLock().lock(); + checkFileAccess(); + if (exists(key)) { + map.put(key, null); + keyValueOffset.remove(key); + keySet.remove(key); + } + readWriteLock.readLock().unlock(); + } + + @Override + public Iterator readKeys() { + readWriteLock.readLock().lock(); + try { + checkFileAccess(); + return keySet.iterator(); + } finally { + readWriteLock.readLock().unlock(); + } + } + + @Override + public int size() { + readWriteLock.readLock().lock(); + try { + checkFileAccess(); + return keySet.size(); + } finally { + readWriteLock.readLock().unlock(); + } + } + + @Override + public void close() throws IOException { + readWriteLock.writeLock().lock(); + opened = false; + try { + pushMapIntoFile(); + offsets = new RandomAccessFile(offsetsFile, "rw"); + offsets.writeInt(keyValueOffset.size()); + for (Map.Entry entry : keyValueOffset.entrySet()) { + keySerialization.write(offsets, entry.getKey()); + offsets.writeLong(entry.getValue()); + } + storage.close(); + offsets.close(); + } finally { + readWriteLock.writeLock().unlock(); + } + } +} diff --git a/homework-g595-ferenets/src/test/java/ru/mipt/java2016/homework/g595/ferenets/task1/MyCalculatorTest.java b/homework-g595-ferenets/src/test/java/ru/mipt/java2016/homework/g595/ferenets/task1/MyCalculatorTest.java new file mode 100644 index 000000000..fbc3ee4c1 --- /dev/null +++ b/homework-g595-ferenets/src/test/java/ru/mipt/java2016/homework/g595/ferenets/task1/MyCalculatorTest.java @@ -0,0 +1,12 @@ +package ru.mipt.java2016.homework.g595.ferenets.task1; + +import ru.mipt.java2016.homework.base.task1.Calculator; +import ru.mipt.java2016.homework.tests.task1.AbstractCalculatorTest; + + +public class MyCalculatorTest extends AbstractCalculatorTest { + @Override + protected Calculator calc() { + return new MyCalculator(); + } +} diff --git a/homework-g595-ferenets/src/test/java/ru/mipt/java2016/homework/g595/ferenets/task2/MyKeyValueStorageTest.java b/homework-g595-ferenets/src/test/java/ru/mipt/java2016/homework/g595/ferenets/task2/MyKeyValueStorageTest.java new file mode 100644 index 000000000..d9dfeb742 --- /dev/null +++ b/homework-g595-ferenets/src/test/java/ru/mipt/java2016/homework/g595/ferenets/task2/MyKeyValueStorageTest.java @@ -0,0 +1,41 @@ +package ru.mipt.java2016.homework.g595.ferenets.task2; + +import ru.mipt.java2016.homework.base.task2.KeyValueStorage; +import ru.mipt.java2016.homework.tests.task2.AbstractSingleFileStorageTest; +import ru.mipt.java2016.homework.tests.task2.Student; +import ru.mipt.java2016.homework.tests.task2.StudentKey; + +import java.io.IOException; + +public class MyKeyValueStorageTest extends AbstractSingleFileStorageTest { + + @Override + protected KeyValueStorage buildStringsStorage(String path) { + try { + return new MyKeyValueStorage(path, new StringSerializationStrategy(), new StringSerializationStrategy()); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + @Override + protected KeyValueStorage buildNumbersStorage(String path) { + try { + return new MyKeyValueStorage(path, new IntegerSerializationStrategy(), new DoubleSerializationStrategy()); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + @Override + protected KeyValueStorage buildPojoStorage(String path) { + try { + return new MyKeyValueStorage(path, new StudentKeySerializationStrategy(), new StudentSerializationStrategy()); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/homework-g595-ferenets/src/test/java/ru/mipt/java2016/homework/g595/ferenets/task3/OptimizedStoragePerformanceTest.java b/homework-g595-ferenets/src/test/java/ru/mipt/java2016/homework/g595/ferenets/task3/OptimizedStoragePerformanceTest.java new file mode 100644 index 000000000..d194708b1 --- /dev/null +++ b/homework-g595-ferenets/src/test/java/ru/mipt/java2016/homework/g595/ferenets/task3/OptimizedStoragePerformanceTest.java @@ -0,0 +1,43 @@ +package ru.mipt.java2016.homework.g595.ferenets.task3; + +import ru.mipt.java2016.homework.base.task2.*; +import ru.mipt.java2016.homework.g595.ferenets.task2.*; +import ru.mipt.java2016.homework.tests.task2.*; +import ru.mipt.java2016.homework.tests.task3.KeyValueStoragePerformanceTest; + +import java.io.IOException; + +public class OptimizedStoragePerformanceTest extends KeyValueStoragePerformanceTest { + @Override + protected KeyValueStorage buildStringsStorage(String path) throws MalformedDataException { + try { + return new MyOptimizedStorage(path, + new StringSerializationStrategy(), new StringSerializationStrategy()); + } catch (IOException e) { + e.getCause(); + } + return null; + } + + @Override + protected KeyValueStorage buildNumbersStorage(String path) throws MalformedDataException { + try { + return new MyOptimizedStorage(path, + new IntegerSerializationStrategy(), new DoubleSerializationStrategy()); + } catch (IOException e) { + e.getCause(); + } + return null; + } + + @Override + protected KeyValueStorage buildPojoStorage(String path) throws MalformedDataException { + try { + return new MyOptimizedStorage(path, + new StudentKeySerializationStrategy(), new StudentSerializationStrategy()); + } catch (IOException e) { + e.getCause(); + } + return null; + } +} diff --git a/pom.xml b/pom.xml index 5bca65ec3..a8e972bb7 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,7 @@ homework-g596-grebenshchikova homework-g597-kochukov homework-g597-sigareva + homework-g595-ferenets workshop-materials