import pytest
import pytz
import copy
import time
from datetime import date, timedelta, datetime as Datetime

import maya
from maya.core import _seconds_or_timedelta  # import private function

def test_rfc2822():
    r = maya.parse('February 21, 1994').rfc2822()
    d = maya.MayaDT.from_rfc2822(r)
    assert r == 'Mon, 21 Feb 1994 00:00:00 GMT'
    assert r == d.rfc2822()

def test_iso8601():
    r = maya.parse('February 21, 1994').iso8601()
    d = maya.MayaDT.from_iso8601(r)
    assert r == '1994-02-21T00:00:00Z'
    assert r == d.iso8601()

def test_parse_iso8601():
    string = '20161001T1430.4+05:30'
    expected = '2016-10-01T09:00:00.400000Z'
    d = maya.MayaDT.from_iso8601(string)

    assert expected == d.iso8601()

    string = '2016T14'
    expected = '2016-01-01T14:00:00Z'
    d = maya.MayaDT.from_iso8601(string)

    assert expected == d.iso8601()

    string = '2016-10T14'
    expected = '2016-10-01T14:00:00Z'
    d = maya.MayaDT.from_iso8601(string)

    assert expected == d.iso8601()

    string = '2012W05'
    expected = '2012-01-30T00:00:00Z'
    d = maya.MayaDT.from_iso8601(string)

    assert expected == d.iso8601()

    string = '2012W055'
    expected = '2012-02-03T00:00:00Z'
    d = maya.MayaDT.from_iso8601(string)

    assert expected == d.iso8601()

    string = '2012007'
    expected = '2012-01-07T00:00:00Z'
    d = maya.MayaDT.from_iso8601(string)

    assert expected == d.iso8601()

    string = '2016-W07T09'
    expected = '2016-02-15T09:00:00Z'
    d = maya.MayaDT.from_iso8601(string)

    assert expected == d.iso8601()

def test_struct():
    ts = time.gmtime()
    m = maya.MayaDT.from_struct(ts)
    dt = Datetime.fromtimestamp(time.mktime(ts), pytz.UTC)
    assert m._epoch != None
    assert m.datetime() == dt

    ts = time.localtime()
    m = maya.MayaDT.from_struct(ts)
    dt = Datetime.fromtimestamp(time.mktime(ts), pytz.UTC)
    assert m._epoch != None
    assert m.datetime() == dt

def test_human_when():
    r1 = maya.when('yesterday')
    r2 = maya.when('today')

    assert (r2.day - r1.day) in (1, -30, -29, -28, -27)

def test_machine_parse():
    r1 = maya.parse('August 14, 2015')
    assert r1.day == 14

    r2 = maya.parse('August 15, 2015')
    assert r2.day == 15

def test_dt_tz_translation():
    d1 = maya.now().datetime()
    d2 = maya.now().datetime(to_timezone='EST')
    assert (d1.hour - d2.hour) % 24 == 5

def test_dt_tz_naive():
    d1 = maya.now().datetime(naive=True)
    assert d1.tzinfo is None

    d2 = maya.now().datetime(to_timezone='EST', naive=True)
    assert d2.tzinfo is None
    assert (d1.hour - d2.hour) % 24 == 5

def test_random_date():

    # Test properties for maya.when()
    d1 = maya.when('11-17-11 08:09:10')
    assert d1.year == 2011
    assert d1.month == 11
    assert d1.day == 17
    assert d1.week == 46
    assert d1.weekday == 4
    assert d1.hour == 8
    assert d1.minute == 9
    assert d1.second == 10
    assert d1.microsecond == 0

    # Test properties for maya.parse()
    d2 = maya.parse('February 29, 1992 13:12:34')
    assert d2.year == 1992
    assert d2.month == 2
    assert d2.day == 29
    assert d2.week == 9
    assert d2.weekday == 6
    assert d2.hour == 13
    assert d2.minute == 12
    assert d2.second == 34
    assert d2.microsecond == 0

def test_print_date(capsys):
    d = maya.when('11-17-11')

    print(d)
    out, err = capsys.readouterr()

    assert out == 'Thu, 17 Nov 2011 00:00:00 GMT\n'
    assert repr(d) == '<MayaDT epoch=1321488000.0>'

def test_invalid_date():
    with pytest.raises(ValueError):
        maya.when('another day')

def test_slang_date():
    d = maya.when('tomorrow')
    assert d.slang_date() == 'tomorrow'

def test_slang_time():
    d = maya.when('one hour ago')
    assert d.slang_time() == 'an hour ago'

def test_parse():
    d = maya.parse('February 21, 1994')
    assert format(d) == '1994-02-21 00:00:00+00:00'

    d = maya.parse('01/05/2016')
    assert format(d) == '2016-01-05 00:00:00+00:00'

    d = maya.parse('01/05/2016', day_first=True)
    assert format(d) == '2016-05-01 00:00:00+00:00'

    d = maya.parse('2016/05/01', year_first=True, day_first=False)
    assert format(d) == '2016-05-01 00:00:00+00:00'

    d = maya.parse('2016/01/05', year_first=True, day_first=True)
    assert format(d) == '2016-05-01 00:00:00+00:00'

    d = maya.parse('01/05/2016', timezone='UTC')
    assert format(d) == '2016-01-05 00:00:00+00:00'

    d = maya.parse('01/05/2016', timezone='US/Central')
    assert format(d) == '2016-01-05 06:00:00+00:00'

def test_when_past():
    next_month = str(maya.now().add(months=1).month)
    this_year = maya.now().year
    last_year = this_year - 1

    future_date = maya.when(next_month)
    past_date = maya.when(next_month, prefer_past=True)

    assert future_date.year == this_year
    if next_month == '1':
        assert past_date.year == this_year
    else:
        assert past_date.year == last_year

def test_datetime_to_timezone():
    dt = maya.when('2016-01-01').datetime(to_timezone='US/Eastern')
    assert dt.tzinfo.zone == 'US/Eastern'

def test_rfc3339():
    mdt =  maya.when('2016-01-01')
    out = mdt.rfc3339()
    mdt2 = maya.MayaDT.from_rfc3339(out)
    assert mdt.epoch == mdt2.epoch

def test_comparison_operations():
    now = maya.now()
    now_copy = copy.deepcopy(now)
    tomorrow = maya.when('tomorrow')

    assert (now == now_copy) is True
    assert (now == tomorrow) is False

    assert (now != now_copy) is False
    assert (now != tomorrow) is True

    assert (now < now_copy) is False
    assert (now < tomorrow) is True

    assert (now <= now_copy) is True
    assert (now <= tomorrow) is True

    assert (now > now_copy) is False
    assert (now > tomorrow) is False

    assert (now >= now_copy) is True
    assert (now >= tomorrow) is False

    # Check Exceptions
    with pytest.raises(TypeError):
        now == 1
    with pytest.raises(TypeError):
        now != 1
    with pytest.raises(TypeError):
        now < 1
    with pytest.raises(TypeError):
        now <= 1
    with pytest.raises(TypeError):
        now > 1
    with pytest.raises(TypeError):
        now >= 1

def test_seconds_or_timedelta():
    # test for value in seconds
    assert _seconds_or_timedelta(1234) == timedelta(0, 1234)
    # test for value as `datetime.timedelta`
    assert _seconds_or_timedelta(timedelta(0, 1234)) == timedelta(0, 1234)
    # test for invalid value
    with pytest.raises(TypeError):
        _seconds_or_timedelta('invalid interval')

def test_intervals():
    now = maya.now()
    tomorrow = now.add(days=1)

    assert len(list(maya.intervals(now, tomorrow, 60*60))) == 24

def test_dunder_add():
    now = maya.now()
    assert now + 1 == now.add(seconds=1)
    assert now + timedelta(seconds=1) == now.add(seconds=1)

def test_dunder_radd():
    now = maya.now()
    assert now.add(seconds=1) == now + 1
    assert now.add(seconds=1) == now + timedelta(seconds=1)

def test_dunder_sub():
    now = maya.now()
    assert now - 1 == now.subtract(seconds=1)
    assert now - timedelta(seconds=1) == now.subtract(seconds=1)

def test_core_local_timezone(monkeypatch):
    @property
    def mock_local_tz(self):
        class StaticTzInfo(object):
            zone = 'local'
            def __repr__(self):
                return "<StaticTzInfo 'local'>"
        return StaticTzInfo()
    monkeypatch.setattr(maya.MayaDT, '_local_tz', mock_local_tz)
    mdt = maya.MayaDT(0)
    assert mdt.local_timezone == 'UTC'


from tkinter import *
import numpy as np
import matplotlib.pyplot as plt
#im = np.zeros([28, 28])
def draw():
    root = Tk()
    image = np.zeros([280, 280])
    # 创建一个框架，在这个框架中响应事件
    # frame = Frame(root,width=280, height=280)
    canvas = Canvas(root, width=280, height=280, bg='white')
    canvas.pack()

    def callBack(event):
        try:
            """
            for i in range(40):
                for j in range(40):
                    if(i == 20 and j == 20):
                        image[event.y, event.x] = 255
                    else:
                        image[event.y - 20 + j, event.x - 20 + i] = 255 / np.sqrt((20-j)*(20-j) + (20-i)*(20-i))
            image[event.y - 10:event.y + 10, event.x - 10:event.x + 10] = 255
            """
            image[event.y - 10:event.y + 10, event.x - 10:event.x + 10] = image[event.y - 10:event.y + 10,
                                                                          event.x - 10:event.x + 10] + 30

        except:
            pass
        canvas.create_oval(event.x - 20, event.y - 20, event.x + 20, event.y + 20, fill='black')
        # print("现在的位置是", event.x, event.y)  # 按哪个键，在Shell中打印

    canvas.bind("<B1-Motion>", callBack)
    # frame.bind("<B1-Motion>", callBack)
    # frame.pack()
    small_image = np.zeros([28, 28])

    def displayClear(event):
        image[image > 255] = 255
        for i in range(28):
            for j in range(28):
                temp = image[i * 10: (i + 1) * 10, j * 10: (j + 1) * 10]
                #small_image[i, j] = temp.sum() / temp.size
                small_image[i, j] = temp.sum() / (temp[temp > 0].size if temp[temp > 0].size > 0 else 1)
        """
        fig = plt.figure()
        plotwindow = fig.add_subplot(111)
        plt.imshow(im, cmap='gray')
        plt.show()
        # canvas.delete(ALL)
        """
        #im = small_image / small_image.max() * 255
        im = small_image
        print('im.size is :', im.size)
        #im.reshape((1, 784))
        file = open('draw.txt', 'w')
        for i in im:
            for j in i:
                file.write(str(int(j)) + '\n')
                #print(j, end=' ')
            #file.write('\r\n')
            #print('\r\n')
        file.close()

        # canvas.destroy()
        #im = small_image / small_image.max()
        # im = im.reshape((1, 784))
        root.destroy()

    canvas.bind("<Button-3>", displayClear)

    mainloop()


import pandas as pd
import argparse
from processor.Analyzer import Analyzer
from processor.Reader import Reader
from processor.Cleaner import Cleaner
from processor.ColumnRemover import ColumnRemover
from processor.Writer import Writer
import logging

logging.basicConfig(
    level=logging.DEBUG, format="'%(asctime)s - %(levelname)s - %(message)s"
)

class Main:
    """
    Handles coordinating and running the application.
    It receives command line arguments and
        1. Reads in source data, transforming it into a dataframe
        2. Removes excess columns
        3. Cleans the dataframe
        4. Runs the analysis (queries)
        5. Writes the query results to the output directory
    """

    def __init__(
        self, data_loc, remove_useless_cols, fill_strategy, output_dir, output_fmt
    ):
        self.data_loc = data_loc
        self.remove_useless_cols = remove_useless_cols
        self.fill_strategy = fill_strategy
        self.output_dir = output_dir
        self.output_fmt = output_fmt
        self.writer = Writer(self.output_dir)
        self.groups = ["payment_type", "year", "month"]

    def do_read(self):
        r = Reader(self.data_loc)
        df = r.reader()
        return df

    def do_column_removal(self, df):
        cr = ColumnRemover(df, self.remove_useless_cols)
        df = cr.df
        return df

    def do_clean(self, df):
        c = Cleaner(df, self.fill_strategy)
        c.handle_null_rows()
        df = c.df
        return df

    def initial_processing(self):
        df = self.do_read()
        df = self.do_column_removal(df)
        df = self.do_clean(df)
        return df

    def analyze(self, analyzer, group):
        df = analyzer.group_by(group)
        return df

    def main(self):
        df = self.initial_processing()
        a = Analyzer(df)
        for group in self.groups:
            output_df = self.analyze(a, group)
            self.writer.write(output_df, group, self.output_fmt)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Presents summary statistics")
    parser.add_argument("-data_source", help="path to source data", required=True)
    parser.add_argument("-data_dest", help="path to output data dir", required=True)
    parser.add_argument(
        "-output_format",
        help="Format of output dataframes (csv, parquet)",
        choices=["csv", "parquet"],
        required=True,
    )
    parser.add_argument(
        "-remove_useless_cols",
        help="Remove cost-related columns that have no values",
        action="store_true",
    )
    parser.add_argument(
        "-fill_strategy",
        help="How to handle null values in cost-related columns (remove, mean)",
        choices=["remove", "mean"],
        required=True,
    )

    args = parser.parse_args()
    data_source = args.data_source
    data_dest = args.data_dest
    output_format = args.output_format
    remove_useless_cols = args.remove_useless_cols
    fill_strategy = args.fill_strategy
    logging.info(
        f"Running with -data_source {data_source} -data_dest {data_dest} -output_format {output_format} -remove_useless_cols {remove_useless_cols} -fill_strategy {fill_strategy}"
    )

    m = Main(data_source, remove_useless_cols, fill_strategy, data_dest, output_format)
    m.main()


# import required modules
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from youtube_transcript_api import YouTubeTranscriptApi

import os
import pickle
import pandas as pd
import sys

SCOPES = ["https://www.googleapis.com/auth/youtube.force-ssl"]

# Function to authorize API access using OAuth2
def youtube_authenticate():
    os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
    api_service_name = "youtube"
    api_version = "v3"
    client_secrets_file = "credentials.json" #change with credentials.json
    creds = None
    # the file token.pickle stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first time
    if os.path.exists("token.pickle"):
        with open("token.pickle", "rb") as token:
            creds = pickle.load(token)
    # if there are no (valid) credentials availablle, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(client_secrets_file, SCOPES)
            creds = flow.run_local_server(port=0)
        # save the credentials for the next run
        with open("token.pickle", "wb") as token:
            pickle.dump(creds, token)

    return build(api_service_name, api_version, credentials=creds)

# Function to search for videos based on a keyword
def search_videos(service, threshold_api_units, **kwargs):
    all_results = []
    next_page_token = ''

    total_api_units_used = 0
    print("Total units used: %d" % total_api_units_used)

    while total_api_units_used < threshold_api_units and next_page_token is not None:
        # Calculate the number of API units needed for the upcoming request (e.g., 100 units for search)
        api_units_needed = 100

        # Check if making the next request would exceed the threshold
        if total_api_units_used + api_units_needed <= threshold_api_units:

            search_results = service.search().list(**kwargs).execute()
            all_results.extend(search_results.get('items', []))

            total_api_units_used += api_units_needed
            print("Total units used: %d" % total_api_units_used)

            if total_api_units_used + api_units_needed <= threshold_api_units:
            # Check for more pages of results
                next_page_token = search_results.get('nextPageToken')
                total_api_units_used += api_units_needed
                print("Total units used: %d" % total_api_units_used)
                if not next_page_token:
                    break
                kwargs['pageToken'] = next_page_token
            else:
                print("Reached API unit threshold. Stopping requests.")
                break
        else:
            print("Reached API unit threshold. Stopping requests.")
            break

    return all_results

# Function to retrieve video details
def get_video_details(service, video_id):
    video_details = service.videos().list(part='snippet,statistics', id=video_id).execute()
    return video_details.get('items', [])[0] if video_details.get('items', []) else None

if __name__ == '__main__':

    KEYWORD = "vegan"
    START_DATE = "2022-12-01T00:00:00Z"
    END_DATE = "2023-03-01T00:00:00Z"

    youtube = youtube_authenticate()

    # Step 1: Search for videos based on the keyword
    search_results = search_videos(youtube, threshold_api_units = 8000, q=KEYWORD, publishedAfter=START_DATE, publishedBefore=END_DATE, relevanceLanguage="en", type="video", part='id', maxResults=50, order="date")

    video_data = []

    for video in search_results:
        video_id = video['id']['videoId']

        # Step 2: Retrieve video details
        video_details = get_video_details(youtube, video_id)

        if video_details:
            video_title = video_details['snippet']['title']
            video_description = video_details['snippet']['description']
            video_timestamp = video_details['snippet']['publishedAt']
            video_likes = video_details['statistics']['likeCount']

            video_data.append({
                'Video ID': video_id,
                'Video Title': video_title,
                'Video Timestamp': video_timestamp,
                'Video Description': video_description,
                'Video Likes': video_likes
            })

    # Create a DataFrame-like structure using pandas
    df = pd.DataFrame(video_data)

    print(df.head())
    print(df["Video Timestamp"].min())
    print(df.shape)

    # get transcripts if automatic caption is available
    video_ids = list(df["Video ID"].values)

    transcript_list, unretrievable_videos = YouTubeTranscriptApi.get_transcripts(video_ids, continue_after_error=True)

    list_transcripts = []

    for video_id in video_ids:

        if video_id in transcript_list.keys():

            srt = transcript_list.get(video_id)

            text_list = []
            for i in srt:
                text_list.append(i['text'])

            text = '.'.join(text_list)
            list_transcripts.append(text)

        else:
            list_transcripts.append(None)

    df["Video Transcript"] = list_transcripts

    retrieved_videos = df

    with open("./data/retrieved_video_2022_key_"+str(KEYWORD)+".pickle", "wb") as token:
        pickle.dump(retrieved_videos, token)


from plone.server import utils
from plone.server.testing import FakeRequest
from plone.server.transactions import get_current_request

import gc
import resource

def test_module_resolve_path():
    assert utils.resolve_module_path('plone.server') == 'plone.server'
    assert utils.resolve_module_path('plone.server.tests') == 'plone.server.tests'
    assert utils.resolve_module_path('..test_queue') == 'plone.server.tests.test_queue'
    assert utils.resolve_module_path('....api') == 'plone.server.api'

class TestGetCurrentRequest:
    def test_gcr_memory(self):
        self.request = FakeRequest()

        count = 0
        current = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.0 / 1024.0
        while True:
            count += 1
            get_current_request()

            if count % 1000000 == 0:
                break

            if count % 100000 == 0:
                gc.collect()
                new = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.0 / 1024.0
                if new - current > 10:  # memory leak, this shouldn't happen
                    assert new == current


import math
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

from arcade import Texture, load_texture
from arcade.hitbox import HitBox, RotatableHitBox
from arcade.math import get_angle_degrees
from arcade.texture import get_default_texture
from arcade.types import PathOrTexture, Point

from .base import BasicSprite
from .mixins import PymunkMixin

if TYPE_CHECKING:  # handle import cycle caused by type hinting
    from arcade.sprite_list import SpriteList

class Sprite(BasicSprite, PymunkMixin):
    """
    Class that represents a 'sprite' on-screen. Most games center around sprites.
    For examples on how to use this class, see:
    https://api.arcade.academy/en/latest/examples/index.html#sprites

    :param str path_or_texture: Path to the image file, or a texture object.
    :param float center_x: Location of the sprite
    :param float center_y: Location of the sprite
    :param float scale: Scale the image up or down. Scale of 1.0 is none.
    :param float angle: The initial rotation of the sprite in degrees
    """

    __slots__ = (
        "_velocity",
        "change_angle",
        "_properties",
        "boundary_left",
        "boundary_right",
        "boundary_top",
        "boundary_bottom",
        "textures",
        "cur_texture_index",
        "_hit_box",
        "physics_engines",
        "_sprite_list",
        "guid",
        "force",
    )

    def __init__(
        self,
        path_or_texture: PathOrTexture = None,
        scale: float = 1.0,
        center_x: float = 0.0,
        center_y: float = 0.0,
        angle: float = 0.0,
        **kwargs,
    ):
        if isinstance(path_or_texture, Texture):
            _texture = path_or_texture
            _textures = [_texture]
        elif isinstance(path_or_texture, (str, Path)):
            _texture = load_texture(path_or_texture)
            _textures = [_texture]
        else:
            _texture = get_default_texture()
            # Backwards compatibility:
            # When applying default texture we don't want
            # it part of the animating ones
            _textures = []
        super().__init__(
            _texture,
            scale=scale,
            center_x=center_x,
            center_y=center_y,
            **kwargs,
        )
        PymunkMixin.__init__(self)

        self._angle = angle
        # Movement
        self._velocity = 0.0, 0.0
        self.change_angle: float = 0.0

        # Custom sprite properties
        self._properties: Optional[Dict[str, Any]] = None

        # Boundaries for moving platforms in tilemaps
        self.boundary_left: Optional[float] = None
        self.boundary_right: Optional[float] = None
        self.boundary_top: Optional[float] = None
        self.boundary_bottom: Optional[float] = None

        self.cur_texture_index: int = 0
        self.textures: List[Texture] = _textures

        self.physics_engines: List[Any] = []

        self._sprite_list: Optional[SpriteList] = None
        # Debug properties
        self.guid: Optional[str] = None

        self._hit_box: RotatableHitBox = self._hit_box.create_rotatable(
            angle=self._angle
        )

        self._width = self._texture.width * scale
        self._height = self._texture.height * scale

    # --- Properties ---

    @property
    def angle(self) -> float:
        """
        Get or set the rotation or the sprite.

        The value is in degrees and is clockwise.
        """
        return self._angle

    @angle.setter
    def angle(self, new_value: float):
        if new_value == self._angle:
            return

        self._angle = new_value
        self._hit_box.angle = new_value

        for sprite_list in self.sprite_lists:
            sprite_list._update_angle(self)

        self.update_spatial_hash()

    @property
    def radians(self) -> float:
        """
        Get or set the rotation of the sprite in radians.

        The value is in radians and is clockwise.
        """
        return self._angle / 180.0 * math.pi

    @radians.setter
    def radians(self, new_value: float):
        self.angle = new_value * 180.0 / math.pi

    @property
    def velocity(self) -> Point:
        """
        Get or set the velocity of the sprite.

        The x and y velocity can also be set separately using the
        ``sprite.change_x`` and ``sprite.change_y`` properties.

        Example::

            sprite.velocity = 1.0, 0.0

        Returns:
            Tuple[float, float]
        """
        return self._velocity

    @velocity.setter
    def velocity(self, new_value: Point):
        self._velocity = new_value

    @property
    def change_x(self) -> float:
        """Get or set the velocity in the x plane of the sprite."""
        return self.velocity[0]

    @change_x.setter
    def change_x(self, new_value: float):
        self._velocity = new_value, self._velocity[1]

    @property
    def change_y(self) -> float:
        """Get or set the velocity in the y plane of the sprite."""
        return self.velocity[1]

    @change_y.setter
    def change_y(self, new_value: float):
        self._velocity = self._velocity[0], new_value

    @property
    def hit_box(self) -> HitBox:
        """
        Get or set the hit box for this sprite.
        """
        return self._hit_box

    @hit_box.setter
    def hit_box(self, hit_box: Union[HitBox, RotatableHitBox]):
        if type(hit_box) == HitBox:
            self._hit_box = hit_box.create_rotatable(self.angle)
        else:
            # Mypy doesn't seem to understand the type check above
            # It still thinks hit_box can be a union here
            self._hit_box = hit_box  # type: ignore

    @property
    def texture(self) -> Texture:
        """Get or set the active texture for this sprite"""
        return self._texture

    @texture.setter
    def texture(self, texture: Texture):
        if texture == self._texture:
            return

        if __debug__ and not isinstance(texture, Texture):
            raise TypeError(
                f"The 'texture' parameter must be an instance of arcade.Texture,"
                f" but is an instance of '{type(texture)}'."
            )

        # If sprite is using default texture, update the hit box
        if self._texture is get_default_texture():
            self.hit_box = RotatableHitBox(
                texture.hit_box_points,
                position=self._position,
                angle=self.angle,
                scale=self._scale,
            )

        self._texture = texture
        self._width = texture.width * self._scale[0]
        self._height = texture.height * self._scale[1]
        self.update_spatial_hash()
        for sprite_list in self.sprite_lists:
            sprite_list._update_texture(self)

    @property
    def properties(self) -> Dict[str, Any]:
        """
        Get or set custom sprite properties.

        :rtype: Dict[str, Any]
        """
        if self._properties is None:
            self._properties = {}
        return self._properties

    @properties.setter
    def properties(self, value):
        self._properties = value

    # --- Movement methods -----

    def forward(self, speed: float = 1.0) -> None:
        """
        Adjusts a Sprites forward.

        :param speed: speed
        """
        angle_rad = math.radians(self.angle)
        self.center_x += math.sin(angle_rad) * speed
        self.center_y += math.cos(angle_rad) * speed

    def reverse(self, speed: float = 1.0) -> None:
        """
        Adjusts a Sprite backwards.

        :param speed: speed
        """
        self.forward(-speed)

    def strafe(self, speed: float = 1.0) -> None:
        """
        Adjusts a Sprite sideways.

        :param speed: speed
        """
        angle_rad = math.radians(self.angle + 90)
        self.center_x += math.sin(angle_rad) * speed
        self.center_y += math.cos(angle_rad) * speed

    def turn_right(self, theta: float = 90.0) -> None:
        """
        Rotate the sprite right by the passed number of degrees.

        :param theta: change in angle, in degrees
        """
        self.angle = self._angle + theta

    def turn_left(self, theta: float = 90.0) -> None:
        """
        Rotate the sprite left by the passed number of degrees.

        :param theta: change in angle, in degrees
        """
        self.angle = self._angle - theta

    def stop(self) -> None:
        """
        Stop the Sprite's motion by setting the velocity and angle change to 0.
        """
        self.velocity = 0, 0
        self.change_angle = 0

    def face_point(self, point: Point) -> None:
        """
        Face the sprite towards a point. Assumes sprite image is facing upwards.

        :param Point point: Point to face towards.
        """
        angle = get_angle_degrees(self.center_x, self.center_y, point[0], point[1])

        # Reverse angle because sprite angles are backwards
        self.angle = -angle

    # ---- Draw Methods ----

    def draw(self, *, filter=None, pixelated=None, blend_function=None) -> None:
        """
        Draw the sprite.

        :param filter: Optional parameter to set OpenGL filter, such as
                       `gl.GL_NEAREST` to avoid smoothing.
        :param pixelated: ``True`` for pixelated and ``False`` for smooth interpolation.
                          Shortcut for setting filter=GL_NEAREST.
        :param blend_function: Optional parameter to set the OpenGL blend function used for drawing the sprite list,
                               such as 'arcade.Window.ctx.BLEND_ADDITIVE' or 'arcade.Window.ctx.BLEND_DEFAULT'
        """
        if self._sprite_list is None:
            from arcade import SpriteList

            self._sprite_list = SpriteList(capacity=1)

        self._sprite_list.append(self)
        self._sprite_list.draw(
            filter=filter, pixelated=pixelated, blend_function=blend_function
        )
        self._sprite_list.remove(self)

    # ----Update Methods ----

    def update(self) -> None:
        """
        The default update method for a Sprite. Can be overridden by a subclass.

        This method moves the sprite based on its velocity and angle change.
        """
        self.position = (
            self._position[0] + self.change_x,
            self._position[1] + self.change_y,
        )
        self.angle += self.change_angle

    # ----Utility Methods----

    def update_spatial_hash(self) -> None:
        """
        Update the sprites location in the spatial hash.
        """
        # self._hit_box._adjusted_cache_dirty = True
        # super().update_spatial_hash()
        for sprite_list in self.sprite_lists:
            if sprite_list.spatial_hash is not None:
                sprite_list.spatial_hash.move(self)

    def append_texture(self, texture: Texture):
        """
        Appends a new texture to the list of textures that can be
        applied to this sprite.

        :param arcade.Texture texture: Texture to add to the list of available textures

        """
        self.textures.append(texture)

    def set_texture(self, texture_no: int) -> None:
        """
        Set the current texture by texture number.
        The number is the index into ``self.textures``.

        :param int texture_no: Index into ``self.textures``
        """
        texture = self.textures[texture_no]
        self.texture = texture

    def remove_from_sprite_lists(self) -> None:
        """
        Remove this sprite from all sprite lists it is in
        including registered physics engines.
        """
        super().remove_from_sprite_lists()
        for engine in self.physics_engines:
            engine.remove_sprite(self)

        self.physics_engines.clear()

    def register_physics_engine(self, physics_engine) -> None:
        """
        Register a physics engine on the sprite.
        This is only needed if you actually need a reference
        to your physics engine in the sprite itself.
        It has no other purposes.

        The registered physics engines can be accessed
        through the ``physics_engines`` attribute.

        It can for example be the pymunk physics engine
        or a custom one you made.
        """
        self.physics_engines.append(physics_engine)


import sqlite3
DataPath="FoodDataBase.db"
SchemaPath="Schema.py"
Folder="DataB\\"
class Refrence:
	def __init__(self,CTFK,FT,FTPK):
		self.TableForeignKey=CTFK
		self.ForeignTable=FT
		self.ForeignTablePrimaryKey=FTPK
	def __str__(self):
		return '''FOREIGN KEY ({0}) REFERENCES {1}({2}) ON UPDATE CASCADE ON DELETE CASCADE'''.format(self.TableForeignKey,self.ForeignTable,self.ForeignTablePrimaryKey)
	__repr__ = __str__

class Table:

	def __init__(self,tablename):
		self.TableName=tablename
		self.col={}
		self.ref=[]

	def AddCol(self, ColAsStr):
		c = str.split(ColAsStr," ")
		#print (c)
		if len(c)>=2:
			return self.AddColByParts(c[0],c[1])
		else:
			return None
	def AddColByParts(self,ColName, ColType):
		if self != None:
			self.col[ColName]=ColType
		return self
	def AddReff(self,TableForeignKey,ForeignTable):
		if self != None:
			self.ref.append(Refrence(TableForeignKey,ForeignTable,"ID"))
			#print(self)
			self.AddColByParts(TableForeignKey,"integer")
		return self
	def __str__(self):
		if self != None:
			c= []
			for clm in self.col.keys():
				c.append(clm +" "+self.col[clm])
			s= '''CREATE TABLE IF NOT EXISTS {0} (ID integer PRIMARY KEY AUTOINCREMENT, {1}'''.format(self.TableName,", ".join(c))
		#	print (c)
			for x in self.ref:
				s+=", "+str(x)
			s+=");"
			return s
		else:
			return "error"
	__repr__ = __str__

def Check():
	global DataPath
	global SchemaPath
	global Folder
	print (DataPath+" "+Folder+SchemaPath)
	conn = sqlite3.connect(DataPath)
	c= conn.cursor()
	Company = Table("Company")
	Company.AddCol("CompanyName text")
	c.execute(str(Company))
	Tutor = Table("Tutor")
	Tutor.AddCol("TutorGroup integer").AddCol("TutorTeacher text").AddReff("Company",Company.TableName)
	c.execute(str(Tutor))
	Customer= Table("Customer")
	Customer.AddCol("CustomerName text").AddReff("Tutor",Tutor.TableName)
	c.execute(str(Customer))
	Transac = Table("Transac")
	Transac.AddCol("Time timestamp").AddReff("Customer",Customer.TableName)
	c.execute(str(Transac))
	FoodSupplyer= Table("FoodSupplyer")
	FoodSupplyer.AddCol("SupplyerName Text").AddCol("Address text").AddCol("Email text").AddCol("Tell text")
	c.execute(str(FoodSupplyer))
	FoodIngredent = Table("FoodIngredent")
	FoodIngredent.AddCol("Ingredent Name").AddCol("PricePerUnit integer").AddReff("Supplyer",FoodSupplyer.TableName)
	c.execute(str(FoodIngredent))
	FoodOrder = Table("FoodOrder")
	FoodOrder.AddReff("Transac",Transac.TableName)
	FoodOrder.AddReff("FoodIngredent",FoodIngredent.TableName)
	c.execute(str(FoodOrder))
	c.close()
	conn.commit()
	f = open(Folder+SchemaPath,"wb")
	s="#{0}\r\n#{1}\r\n#{2}\r\n#{3}\r\n#{4}\r\n#{5}\r\n#{6}\r\n".format(str(Company),str(Tutor),str(Customer),str(Transac),str(FoodSupplyer),str(FoodIngredent),str(FoodOrder))
	s+=LoadImports()
	s+=CreatePyClass(Company)
	s+=CreatePyClass(Tutor)
	s+=CreatePyClass(Customer)
	s+=CreatePyClass(Transac)
	s+=CreatePyClass(FoodSupplyer)
	s+=CreatePyClass(FoodIngredent)
	s+=CreatePyClass(FoodOrder)
	f.write(bytes(s, 'UTF-8'))
	f.close()
	print ("checked")
def LoadImports():
	global DataPath
	global SchemaPath
	global Folder
	return '''import sqlite3\r\nimport itertools\r\nimport functools\r\nDataPath="{0}"\r\n'''.format(DataPath)
def addTestData():
	global DataPath
	conn = sqlite3.connect(DataPath)
	c= conn.cursor()
	for x in range(20):
		print("3")
		c.execute('''INSERT INTO Company(CompanyName) VALUES (?);''',("test"+str(x),))
		print("4")
		c.execute('''INSERT INTO Tutor(Company,TutorGroup,TutorTeacher) VALUES (?,?,?);''',(int(x/2),x,"dave"+str(x)))
	c.close()
	conn.commit()
def getTestTableDB():
	global DataPath
	conn = sqlite3.connect(DataPath)
	c= conn.cursor()
	c.execute('''SELECT CompanyName,TutorGroup,TutorTeacher FROM Company INNER JOIN Tutor on Tutor.Company= Company.ID WHERE Company.ID==1;''')
	vals = c.fetchall()
	c.close()
	return vals

def CreatePyClass(_Table):
	rs='''class {0}:\r\n\tdef __init__(self,ID):\r\n\t\t"""{1}"""\r\n\t\tself.ID=ID\r\n{2}'''
	doc=_Table.TableName+ " Table Contains columns: "
	Atributes="\t@classmethod\r\n\tdef GetAllKeys(cls):\r\n\t\tglobal DataPath\r\n\t\tconn = sqlite3.connect(DataPath)\r\n\t\tc= conn.cursor()\r\n\t\tc.execute('''SELECT ID FROM {0}''')\r\n\t\tVal= c.fetchall()\r\n\t\tc.close()\r\n\t\tif len(Val)>0:\r\n\t\t\treturn list(functools.reduce(itertools.chain,Val))\r\n\t\telse:\r\n\t\t\treturn []\r\n".format(_Table.TableName)
	no_of_col=len(_Table.col.keys())
	Atributes+='''\t@classmethod\r\n\tdef CreateNode(cls):\r\n\t\tglobal DataPath\r\n\t\tconn = sqlite3.connect(DataPath)\r\n\t\tc= conn.cursor()\r\n\t\tc.execute("INSERT INTO {0}({1}) VALUES ({2})",({3},))\r\n\t\tNewID=c.lastrowid\r\n\t\tc.close()\r\n\t\tconn.commit()\r\n\t\treturn cls(NewID)\r\n\tdef Remove(self):\r\n\t\tglobal DataPath\r\n\t\tconn = sqlite3.connect(DataPath)\r\n\t\tc= conn.cursor()\r\n\t\tc.execute("DELETE FROM {0} WHERE ID=?;",(self.ID,))\r\n\t\tc.close()\r\n\t\tconn.commit()\r\n\t\tself.ID=None\r\n'''.format(_Table.TableName,",".join(_Table.col.keys()),"?"+",?"*(no_of_col-1),"None"+",None"*(no_of_col-1))
	strcast='''\tdef TableJson(self):\r\n\t\tr={"ID":self.ID}\r\n'''
	expectedkeys='''\t@classmethod\r\n\tdef expectedkeys(cls):\r\n\t\tr=["ID"]\r\n'''
	for x in _Table.col.keys():
		doc+=x+" of type "+_Table.col[x]+" || "
		Atributes+="\t@property\r\n\tdef {0}(self):\r\n\t\tglobal DataPath\r\n\t\tconn = sqlite3.connect(DataPath)\r\n\t\tc= conn.cursor()\r\n\t\tc.execute('''SELECT {0} FROM {1} WHERE ID=?''',(self.ID,))\r\n\t\tVal= c.fetchone()\r\n\t\tc.close()\r\n\t\tif Val!=None:\r\n\t\t\treturn Val[0]\r\n\t\treturn None\r\n\t@{0}.setter\r\n\tdef {0}(self,value):\r\n\t\tglobal DataPath\r\n\t\tconn = sqlite3.connect(DataPath)\r\n\t\tc= conn.cursor()\r\n\t\tc.execute('''UPDATE {1} SET {0}=? WHERE ID=?''',(value,self.ID))\r\n\t\tc.close()\r\n\t\tconn.commit()\r\n".format(x,_Table.TableName)
		strcast+='''\t\tr["{0}"]=self.{0}\r\n'''.format(x)
		expectedkeys+='''\t\tr.append("{0}")\r\n'''.format(x)
	strcast+='''\t\treturn r\r\n'''
	expectedkeys+='''\t\treturn r\r\n'''
	Atributes+=strcast
	Atributes+=expectedkeys
	Atributes+='''\tdef __str__(self):\r\n\t\treturn str(self.TableJson())\r\n'''
	doc+="Foreign Keys: "
	for x in _Table.ref:
		doc+=_Table.TableName+"."+x.TableForeignKey+" refrences "+x.ForeignTable+"."+x.ForeignTablePrimaryKey
	return rs.format(_Table.TableName,doc,Atributes)
#Check()

#addTestData()

#print(getTestTableDB())


from datetime import date
maiores = 0
menores = 0
ano = date.today().year

for c in range(1, 8):
    nascimento = int(input('Digite o ano de nascimento da {}ª pessoa: '.format(c)))
    if ano - nascimento < 18:
        menores += 1
    else:
        maiores += 1
print('Total de maiores de idade: {}'.format(maiores))
print('Total de menores de idade: {}'.format(menores))


from http import HTTPStatus
from typing import Optional
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Query

from models.filmwork import FilmworkBase
from models.genre import GenreBase, GenreDetail
from services.filmwork import FilmworkService, get_film_service
from services.genre import GenreService, get_genre_service

router = APIRouter()

@router.get(
    "",
    response_model=list[GenreBase],
    summary="Genres list.",
    description="Genres list with pagination.",
    response_description="Name and id.",
    tags=["genre", "objects list"],
)
async def genre_list(
    page: Optional[int] = Query(default=1, alias="page[number]"),
    size: Optional[int] = Query(default=50, alias="page[size]"),
    genre_service: GenreService = Depends(get_genre_service),
) -> list[GenreBase]:
    return await genre_service.get_genres_list(page=page, size=size)

@router.get(
    "/{uuid}/",
    response_model=GenreDetail,
    summary="Get genre by ID.",
    description="Get genre by ID.",
    response_description="Name and id.",
    tags=["genre", "get by id"],
)
async def genre_info(
    uuid: UUID,
    genre_service: GenreService = Depends(get_genre_service),
) -> GenreDetail:
    genre_detail = await genre_service.get_genre(uuid=uuid)
    if not genre_detail:
        raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Genre not found")
    return genre_detail

@router.get(
    "/{name}/film",
    response_model=list[FilmworkBase],
    summary="Get list of movies by genre name.",
    description="Get list of movies by genre name.",
    response_description="Movies name, rating and id.",
    tags=["genre", "film", "objects list"],
)
async def genre_popular_films(
    name: str,
    page: Optional[int] = Query(default=1, alias="page[number]"),
    size: Optional[int] = Query(default=50, alias="page[size]"),
    film_service: FilmworkService = Depends(get_film_service),
) -> list[FilmworkBase]:
    return await film_service.get_filmworks_list(page=page, size=size, filter_genre=name)


from zoo import Zoo
from animals import animal_list
from menu_functions import split, add_animals, info

life_like_a_zoo = Zoo()

running = True
while running:
    print('\nWelcome to my zoo!')
    info()
    action = input('Please enter the number: ')
    if action == '1':
        if life_like_a_zoo.free_area == 0:
            print('Unfortunately there is no free space left at the zoo :(')
        else:
            print(f'Available space: {life_like_a_zoo.free_area} sq.m')
            corral_area = input('Please enter the amount of area that you want to highlight for the corral: ')
            animals_type = input(
                'Please enter the type of the animals that corral is created for\n'
                '1. Herbivore\n'
                '2. Predator\n'
                ':'
            )
            if animals_type == '1':
                life_like_a_zoo.create_corral(int(corral_area), 'herbivore')
            elif animals_type == '2':
                life_like_a_zoo.create_corral(int(corral_area), 'predator')
            else:
                print('Oops,something went wrong...')
    if action == '2':
        if len(life_like_a_zoo.corrals) < 2:
            print(
                "There isn't two corrals at the zoo, if you want to add animals to the corral, you need to "
                "create at least two corrals first"
            )
        else:
            animals = input("Please enter the names of the animals that you want to add: ")
            animals = split(animals)
            add_animals(animal_list, animals, life_like_a_zoo.corrals)
    if action == '3':
        print(life_like_a_zoo)
    if action == '4':
        if life_like_a_zoo.corrals_count < 1:
            print("There is no corrals at the zoo")
        else:
            corral_id = int(input('Please choose corral (enter corral id): '))
            print(life_like_a_zoo.corrals[corral_id])
    if action == '5':
        break


jets = open("17.txt").read().strip()

SHAPES = ["hor", "cross", "l", "ver", "sq"]

def shape_positions(shape, height):
    if shape == "hor":
        pos = [(3, height + 4),(4, height + 4),(5, height + 4),(6, height + 4)]
    elif shape == "cross":
        pos = [(4, height + 4),(4, height + 5),(4, height + 6),(5, height + 5), (3, height + 5)]
    elif shape == "l":
        pos = [(4, height + 4),(3, height + 4),(5, height + 4),(5, height + 5), (5, height + 6)]
    elif shape == "ver":
        pos = [(3, height + 4),(3, height + 5),(3, height + 6),(3, height + 7)]
    else:
        pos = [(3, height + 4),(4, height + 4),(3, height + 5),(4, height + 5)]
    return pos

rocks = {(i, 0) for i in range(1, 8)}
shape = 0
c = 0
i = 1
while True:
    shape_pos = shape_positions(SHAPES[shape], max(rocks, key=lambda x: x[1])[1])
    shape = (shape + 1) % len(SHAPES)
    while True:
        if jets[c] == ">":
            if all((pos[0] + 1, pos[1]) not in rocks and pos[0] < 7 for pos in shape_pos):
                shape_pos = list(map(lambda x: (x[0] + 1, x[1]), shape_pos))
        else:
            if all((pos[0] - 1, pos[1]) not in rocks and pos[0] > 1 for pos in shape_pos):
                shape_pos = list(map(lambda x: (x[0] - 1, x[1]), shape_pos))
        c = (c + 1) % len(jets)
        if all((pos[0], pos[1] - 1) not in rocks for pos in shape_pos):
            shape_pos = list(map(lambda x: (x[0], x[1] - 1), shape_pos))
        else: break

    rocks = rocks.union(set(shape_pos))

    if i == 2022: print (max(rocks, key=lambda x: x[1])[1])

    i += 1


# ---
# jupyter:
#   jupytext:
#     formats: ipynb,py
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.6.0
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # Simple Neural Network - Binary Classification
# ###### Neural network from scratch

# +
import numpy as np
import matplotlib.pyplot as plt

import warnings

# ignore warnings by message (caused by log of zeros/close-to-zero numbers during training):
warnings.filterwarnings('ignore', message='divide by zero encountered in log')
warnings.filterwarnings('ignore', message='invalid value encountered in multiply')

# -

class NN_Model():
    __PRINT_COST_INTERVAL = 2000    # the interval (i.e. iteration #) when the cost should be printed
    __EPSILON = 1e-8                # a small value greater than zero

    def __init__(self,
                 X,
                 Y,
                 hidden_layers,
                 classification_type='binary',
                 learning_rate=0.01,
                 learning_rate_decay_rate=0,
                 num_iterations=10000,
                 regularization=None,
                 lambda_=0.1,
                 initialization=None,
                 optimizer=None,
                 print_cost=False):
        np.random.seed(1)

        self.X = X # inputs
        self.Y = Y # labels

        self.num_iterations = num_iterations
        # we use the term 'initial' learning rate because we will often use learning rate decay:
        self.initial_learning_rate = learning_rate
        # n.b. the decay rate for alpha should be non-negative (otherwise, the learning rate would increase every epoch):
        self.learning_rate_decay_rate = learning_rate_decay_rate if learning_rate_decay_rate >= 0 else 0
        self.print_cost = print_cost                        # boolean - determines if costs per interval should be printed
        self.regularization = str(regularization).upper()   # type of regularization to be implemented
        self.lambda_ = lambda_                              # lambda constant used for L2 regularization equation
        self.classification_type = classification_type      # whether model is binary classification, multi-class classification, etc.

        self.b = {}             # bias unit (trainable); each key represents layer
        self.W = {}             # weights (trainable); each key represents layer
        self.Z = {}             # linear unit; each key represents layer
        self.A = {0: X}         # post-activation unit; each key represents layer; 0th layer is just input layer
        self.db = {}            # gradient of bias unit; each key represents layer
        self.dW = {}            # gradient of weights; each key represents layer
        self.dA = {}            # gradient of post-activation unit; each key represents layer
        self.costs = []         # the cost at a given interval/number of iterations, __PRINT_COST_INTERVAL
        self.m = X.shape[1]     # number of training examples

        self.is_training = True         # boolean to indicate training vs testing/predicting
        self.dropout_mask = {}          # the mask of boolean values (per layer) used for dropout regularization

        # array of layer shapes (nodes per layer), including input and output layers:
        # in binary classification, the last layer is a single neuron (for sigmoid activation), so we append a layer of size 1
        # for multi-class classification (e.g. softmax) the last layer is the size of the number of classes (i.e. the # of rows in Y)
        if classification_type == 'binary':
            self.layers = [{'size': X.shape[0]}] + hidden_layers + [{'size': 1}]
        elif classification_type == 'multi-class' or classification_type == 'multiclass' or classification_type == 'softmax':
            self.layers = [{'size': X.shape[0]}] + hidden_layers + [{'size': Y.shape[0]}]
            # reassign the type to 'softmax' so that we don't have to keep checking variations of the name in our code:
            self.classification_type = 'softmax'
        else:
            raise TypeError('invalid "classification_type" argument -- set parameter as "binary" or "softmax"')

        self.L = len(self.layers)-1  # number of layers

        # for gradient optimization:
        # TODO: make Adam beta values user tunable; though, tuning them is rare:
        self.optimizer = optimizer.upper()
        self.beta_RMS = 0.999
        self.beta_momentum = 0.9
        self.moving_avg_dW = {}  # aka exponentially weighted average of dW -- its "velocity" (v)
        self.moving_avg_db = {}  # aka exponentially weighted average of db -- its "velocity" (v)
        self.RMS_dW = {}  # aka exponentially weighted average of squares of dW
        self.RMS_db = {}  # aka exponentially weighted average of squares of db

        # initialize parameters, W and b:
        for l in range(1, self.L+1):
            # ensure hidden_layers was properly passed as a list of dictionaries:
            # TODO: n.b. code will fail anyway if hidden layers is not properly defined,
            #  so this is check is mostly for the additional error hint and may not be that useful
            if type(self.layers[l]) != dict and 'size' not in self.layers[l]:
                raise TypeError('each item in "layers" must be a dict and must have a property of "size"')

            # determine layer sizes:
            layer_size = self.layers[l]['size']
            previous_layer_size = self.layers[l-1]['size']

            self.W[l] = np.random.randn(layer_size, previous_layer_size)
            self.b[l] = np.zeros((layer_size, 1))

            # weights initialization (scaling weights):
            initialization = str(initialization).upper()

            if initialization == 'HE':
                self.W[l] *= 2 / np.sqrt(previous_layer_size)
            elif initialization == 'XAVIER':
                self.W[l] *= 1 / np.sqrt(previous_layer_size)
            else:
                self.W[l] *= 0.01

            # initialize velocity (if selected) to zeros:
            W_shape = self.W[l].shape
            b_shape = self.b[l].shape
            if self.optimizer == 'MOMENTUM' or self.optimizer == 'ADAM':
                self.moving_avg_dW[l] = np.zeros(W_shape)
                self.moving_avg_db[l] = np.zeros(b_shape)
            if self.optimizer == 'RMSPROP' or self.optimizer == 'ADAM':
                self.RMS_dW[l] = np.zeros(W_shape)
                self.RMS_db[l] = np.zeros(b_shape)

    # ########## model training functions: ##########
    def relu(self, Z):
        return np.maximum(Z, 0)

    def leaky_relu(self, Z):
        return np.where(Z > 0, Z, 0.01*Z)

    def sigmoid(self, Z):
        return 1 / (1 + np.exp(-Z))

    def relu_gradient(self, Z):
        return np.where(Z > 0, 1, 0)

    def leaky_relu_gradient(self, Z):
        return np.where(Z > 0, 1, 0.01)

    def sigmoid_gradient(self, Z):
        return self.sigmoid(Z) * (1 - self.sigmoid(Z))

    def softmax(self, Z):
        # to prevent possible NaN breaking our code:
        Z[np.isnan(Z)] = 0

        t = np.exp(Z)

        # the exponential can become so large that even float64 is unable to handle it, resulting in an overflow warning and resulting in NaN values.
        # the NaN values can be effectively replaced with zeros (and thus, preventing our algorithm from breaking):
        #t[np.isnan(t)] = 0

        return t/np.sum(t, axis=0, keepdims=True)

    def forward_prop(self):
        for l in range(1, self.L+1):
            Z = np.dot(self.W[l], self.A[l-1]) + self.b[l]
            self.Z[l] = np.copy(Z)

            # in the last layer of our network, we apply either sigmoid or softmax activation (for binary or softmax classification, respectively):
            if l == self.L:
                if self.classification_type == 'binary':
                    self.A[l] = self.sigmoid(Z)
                elif self.classification_type == 'softmax':
                    self.A[l] = self.softmax(Z)
            else:
                self.A[l] = self.relu(Z)

            # if dropout is set for layer:
            # need to scale the values of neurons that weren't shut down:
            if self.is_training and (type(self.layers[l]) == dict) and ('keep_prob' in self.layers[l]):
                # create matrix of boolean values (with same shape as A)
                # we set value to `1` if RNG is greater than the probability to keep the neuron (`keep_prob`):
                self.dropout_mask[l] = np.random.random((self.A[l].shape[0], self.A[l].shape[1])) < self.layers[l]['keep_prob']
                self.A[l] *= self.dropout_mask[l] # apply mask
                self.A[l] /= self.layers[l]['keep_prob'] # scale by keep_probability

        Y_hat = self.A[self.L]
        return Y_hat

    def back_prop(self):
        for l in reversed(range(1, self.L+1)):
            if l == self.L:
                dZ = self.A[l] - self.Y

                # # alternatively, we can compute dZ for layer L the long way -- replace the previous line with:
                # self.dA[l] = -(np.divide(self.Y, self.A[l]) - np.divide(1-self.Y, 1-self.A[l]))
                # dZ = self.dA[l] * self.sigmoid_gradient(self.Z[l])
            else:
                dZ = self.dA[l] * self.relu_gradient(self.Z[l])

            self.db[l] = 1.0/self.m * np.sum(dZ, axis=1, keepdims=True)
            self.dA[l-1] = np.dot(self.W[l].T, dZ)
            self.dW[l] = 1.0/self.m * np.dot(dZ, self.A[l-1].T)

            # if dropout is set for layer,
            # apply dropout mask to the same neurons as in forward prop:
            if self.is_training and (type(self.layers[l-1]) == dict) and ('keep_prob' in self.layers[l-1]):
                self.dA[l-1] *= self.dropout_mask[l-1]
                self.dA[l-1] /= self.layers[l-1]['keep_prob']

            # if L2 regularization is set:
            if self.regularization == 'L2':
                 self.dW[l] += self.lambda_/self.m * self.W[l]

    def update_parameters(self, iteration):
        """ Update parameters step during gradient descent. """

        # if decay rate for learning rate (alpha) has been set, update learning rate by epoch:
        if self.learning_rate_decay_rate and iteration % 100 == 0:
            # n.b. there are *several* variations of the learning rate decay formula
            learning_rate = self.initial_learning_rate / (1 + (self.learning_rate_decay_rate * iteration))

            # alternate learning rate decay formula:
            # learning_rate = np.power(0.95, iteration) * self.initial_learning_rate
        else:
            learning_rate = self.initial_learning_rate

        for l in range(1, self.L+1):
            # update either moving avg or RMS of gradients:
            if self.optimizer == 'MOMENTUM' or self.optimizer == 'ADAM':
                self.moving_avg_dW[l] = (self.beta_momentum * self.moving_avg_dW[l]) + ((1-self.beta_momentum) * self.dW[l])
                self.moving_avg_db[l] = (self.beta_momentum * self.moving_avg_db[l]) + ((1-self.beta_momentum) * self.db[l])

                # optional bias correction for exponentially weight avg (i.e. for it to "warm up"):
                moving_avg_dW_corrected = self.moving_avg_dW[l] / (1-np.power(self.beta_momentum, iteration))
                moving_avg_db_corrected = self.moving_avg_db[l] / (1-np.power(self.beta_momentum, iteration))

            if self.optimizer == 'RMSPROP' or self.optimizer == 'ADAM':
                self.RMS_dW[l] = (self.beta_RMS * self.RMS_dW[l]) + ((1-self.beta_RMS) * np.square(self.dW[l]))
                self.RMS_db[l] = (self.beta_RMS * self.RMS_db[l]) + ((1-self.beta_RMS) * np.square(self.db[l]))

                # optional bias correction for exponentially weight avgs of squares:
                RMS_dW_corrected = self.RMS_dW[l] / (1-np.power(self.beta_RMS, iteration))
                RMS_db_corrected = self.RMS_db[l] / (1-np.power(self.beta_RMS, iteration))

            # update parameters based on optimizer algorithm:
            if self.optimizer == 'MOMENTUM':
                self.W[l] -= learning_rate * self.moving_avg_dW[l]
                self.b[l] -= learning_rate * self.moving_avg_db[l]
            elif self.optimizer == 'RMSPROP':
                # n.b. epsilon added to prevent division by zero:
                self.W[l] -= learning_rate * (self.dW[l] / (np.sqrt(RMS_dW_corrected) + self.__EPSILON))
                self.b[l] -= learning_rate * (self.db[l] / (np.sqrt(RMS_db_corrected) + self.__EPSILON))
            elif self.optimizer == 'ADAM':
                # combining moving average of gradients (corrected) and moving average of squared gradients (corrected) gives us Adam:
                # n.b. epsilon added to prevent division by zero:
                self.W[l] -= learning_rate * (moving_avg_dW_corrected / (np.sqrt(RMS_dW_corrected) + self.__EPSILON))
                self.b[l] -= learning_rate * (moving_avg_db_corrected / (np.sqrt(RMS_db_corrected) + self.__EPSILON))
            else:
                self.W[l] -= learning_rate * self.dW[l]
                self.b[l] -= learning_rate * self.db[l]

    def train(self):
        """ Trains the model after initialization. """
        self.is_training = True
        self.costs = []

        self.print_hyperparameters()

        for i in range(1, self.num_iterations+1):
            Y_hat = self.forward_prop()
            self.back_prop()

            if (i == 1) or (i % self.__PRINT_COST_INTERVAL == 0):
                cost = self.compute_cost(Y_hat)
                self.costs.append(cost)

                # we only want to print cost during training for debugging purposes:
                if self.print_cost:
                    print('cost at iteration %s: %s' % (i, cost))

            self.update_parameters(i)

    # ########## public functions: ##########
    def print_hyperparameters(self):
        print('*** Training model with the following hyperparameters: ***')
        print('learning rate (alpha): ', self.initial_learning_rate)
        print('number of iterations:', self.num_iterations)
        if self.optimizer == 'MOMENTUM':
            print('using gradient descent with momentum')
        elif self.optimizer == 'ADAM':
            print('using Adam optimizer')
        else:
            print('using gradient descent')
        print()  # just to create newline

        # TODO: print other hyper parameters
        # TODO: maybe format output a bit?

    def plot_cost(self):
        """
        Plots training costs.

        Used for debugging training algorithm and hyperparameters.
        """

        plt.plot(self.costs)
        plt.title('Cost per Iteration')
        plt.xlabel('Iteration (x%s)' % self.__PRINT_COST_INTERVAL)
        plt.ylabel('Cost')
        plt.show()

    def print_accuracy(self, X, Y):
        """ Prints the accuracy of train/test sets. """
        predictions = self.predict(X)

        accuracy = np.mean(np.int8(predictions == Y))
        print('Accuracy: %s%%' % (accuracy * 100))

# + pycharm={"name": "#%%\n"}
class NN_Binary(NN_Model):
    def __init__(self,
                  X,
                  Y,
                  hidden_layers,
                  classification_type='binary',
                  learning_rate=0.01,
                  learning_rate_decay_rate=0,
                  num_iterations=10000,
                  regularization=None,
                  lambda_=0.1,
                  initialization=None,
                  optimizer=None,
                  print_cost=False):
        NN_Model.__init__(self,
                          X,
                          Y,
                          hidden_layers,
                          classification_type,
                          learning_rate,
                          learning_rate_decay_rate,
                          num_iterations,
                          regularization,
                          lambda_,
                          initialization,
                          optimizer,
                          print_cost)

    def compute_cost(self, Y_hat):
        """
        Computes cost of binary classification (i.e. sigmoid function in last hidden layer)

        If specified, function also computes cost of L2 regularization implementation.

        :param Y_hat: Final layer from forward propagation.
        :return: Cost
        """
        # cross entropy part of cost function:
        cross_entropy_cost = -1/self.m * np.nansum( (self.Y*np.log(Y_hat)) + ((1-self.Y) * np.log(1-Y_hat)) )
        cross_entropy_cost = np.squeeze(cross_entropy_cost)

        if self.regularization == 'L2':
            L2_regularization_cost = 1/self.m * self.lambda_/2 * np.sum([np.sum(np.square(self.W[l])) for l in range(1, self.L+1)])
            cost = cross_entropy_cost + L2_regularization_cost
        else:
            cost = cross_entropy_cost

        return cost

    # ########## public functions: ##########
    def predict(self, X):
        """ Predict """
        self.is_training = False
        self.A[0] = X
        self.m = X.shape[1]

        Y_hat = self.forward_prop()

        predictions = np.where(Y_hat > 0.5, 1, 0)
        return predictions

# + pycharm={"name": "#%%\n"}
# N.b. requisite dimensions of X = (# of features, # of examples)
# N.b. requisite dimensions of Y = (# nodes in fin|al layer, # of examples)
class NN_Softmax(NN_Model):
    def __init__(self,
                  X,
                  Y,
                  hidden_layers,
                  classification_type='softmax',
                  learning_rate=0.01,
                  learning_rate_decay_rate=0,
                  num_iterations=20000,
                  regularization=None,
                  lambda_=0.1,
                  initialization=None,
                  optimizer=None,
                  print_cost=False):

        NN_Model.__init__(self,
                          X,
                          Y,
                          hidden_layers,
                          classification_type,
                          learning_rate,
                          learning_rate_decay_rate,
                          num_iterations,
                          regularization,
                          lambda_,
                          initialization,
                          optimizer,
                          print_cost)

        # TODO: should we convert labels to one-hot encoding inside the instance or during data processing?:
        # self.Y = self.to_one_hot_encoding(self.Y)

    @staticmethod
    def to_one_hot_encoding(labels):
        """ Converts multi-class data to array to one-hot encoding.

            How this method works: The identity matrix is essentially one-hot encoding already.
            We then use array slices to map the labels (array) to the correct indices.

            N.b. the returned identity matrix is transposed so that each column represents a single 1-hot encoding.
            N.b. this function won't work if any of the label values are negative or non-integers.
        """
        assert type(labels) == type(np.array([]))

        # flatten array:
        labels = labels.reshape(-1)

        # need to get the max value because the identity matrix needs to have a unique 1-hot encoding for each value up to the max:
        # the `+ 1` is due to zero-indexing:
        num_values = np.max(labels) + 1
        # n.b. the transpose:
        return np.eye(num_values)[labels].T

    @staticmethod
    def to_categorical(one_hot_encoding):
        """ Converts one-hot encoding back to categorical data.

        :param one_hot_encoding:
        :return:
        """
        assert type(one_hot_encoding) == type(np.array([]))

        return one_hot_encoding.argmax(axis=0)

    def compute_cost(self, Y_hat):
        """
        Computes cost of softmax/multi-class classification.

        TODO: update to also calculate cost w/ L2 regularization implemented.

        :param Y_hat: Output from final layer in forward propagation.
        :return: Cost
        """

        return -1/self.m * np.sum(np.log(Y_hat) * self.Y)

    def predict(self, X):
        """ Predict class """
        self.is_training = False
        self.A[0] = X
        self.m = X.shape[1]

        Y_hat = self.forward_prop()

        # convert predictions (which are probabilities) to one-hot:
        # see <https://stackoverflow.com/a/42397281>:
        predictions = (Y_hat == Y_hat.max(axis=0, keepdims=True)).astype(int)
        return predictions

    def print_accuracy(self, X, Y):
        """ Prints the accuracy of train/test sets. """
        predictions = self.predict(X)

        Y_hat_classes = np.argmax(predictions, axis=0)
        Y_classes = np. argmax(Y, axis=0)

        accuracy = np.mean(np.int8(Y_classes == Y_hat_classes))
        print('Accuracy: %s%%' % (accuracy * 100))

# -

# ## Main
#
# ### Binary Classification:

# + pycharm={"name": "#%%\n"}
# binary classification data:
from data_binary_classification import plot_decision_boundary, load_2D_dataset

# import data for binary classification:
bin_class_train_X, bin_class_train_Y, bin_class_test_X, bin_class_test_Y = load_2D_dataset()

# +
# create model:

# set hyper parameters:
train_hidden_layers = [{'size': 20, 'keep_prob': 0.95}, {'size': 3, 'keep_prob': 1}]

binary_model = NN_Binary(bin_class_train_X,
                        bin_class_train_Y,
                        train_hidden_layers,
                        learning_rate=0.1,
                        learning_rate_decay_rate=1e-4,
                        num_iterations=50000,
                        regularization='l2',
                        initialization='xavier',
                        optimizer='adam',
                        print_cost=True)

# + pycharm={"name": "#%%\n"}
# train model:
binary_model.train()

# + pycharm={"name": "#%%\n"}
# print accuracy:

print('Training set:')
binary_model.print_accuracy(bin_class_train_X, bin_class_train_Y)
print('Test set:')
binary_model.print_accuracy(bin_class_test_X, bin_class_test_Y)

# plot decision boundary after training:

plt.title("Model Decision Boundaries")
axes = plt.gca()
axes.set_xlim([-0.75,0.40])
axes.set_ylim([-0.75,0.65])
plot_decision_boundary(lambda x: binary_model.predict(x.T), bin_class_train_X, np.squeeze(bin_class_train_Y))
# -

# ### Multi-class/Softmax Classification:

# + pycharm={"name": "#%%\n"}
# Spectral Data:
from data_multiclass import generate_spectral_data

multiclass_train_X, multiclass_train_Y = generate_spectral_data()

# transpose input data to correct dimensions:
multiclass_train_X = multiclass_train_X.T

# fix multiclass_train_Y labels to be 0-indexed -- i.e. in range of [0,2] rather than range of [1,3]:
# transform to rank-one array:
multiclass_train_Y -= 1
multiclass_train_Y = multiclass_train_Y.reshape(300, -1)

# convert to one-hot-array:
multiclass_train_Y = NN_Softmax.to_one_hot_encoding(multiclass_train_Y)

# + pycharm={"name": "#%%\n"}
# create model:

# set hyper parameters:
train_hidden_layers = [{'size': 20}, {'size':6}]

multiclass_model = NN_Softmax(multiclass_train_X,
                              multiclass_train_Y,
                              train_hidden_layers,
                              learning_rate=0.1,
                              learning_rate_decay_rate=1e-4,
                              num_iterations=8000,
                              initialization='xavier',
                              optimizer='adam',
                              print_cost=True)

# + pycharm={"name": "#%%\n"}
# train model:
multiclass_model.train()

# + pycharm={"name": "#%%\n"}
# print accuracy:
multiclass_model.print_accuracy(multiclass_train_X, multiclass_train_Y)


class Solution:
    def romanToInt(self, s: str) -> int:
        """
        Convert a Roman numeral string to an integer.

        Parameters:
        s (str): The Roman numeral string to be converted.

        Returns:
        int: The integer representation of the Roman numeral string.

        """
        # Define a dictionary of Roman numeral symbols and their integer values.
        roman_numerals = {
            'I': 1,
            'V': 5,
            'X': 10,
            'L': 50,
            'C': 100,
            'D': 500,
            'M': 1000
        }
        # Initialize the integer representation of the Roman numeral string as 0.
        roman_numeral_as_integer = 0
        # Initialize the index i to 0.
        i = 0
        # While there are still Roman numeral symbols to process.
        while i < len(s):
            # If the current symbol is the last symbol in the string, add its value to the integer representation and exit the loop.
            if i == len(s)-1:
                roman_numeral_as_integer += roman_numerals[ s[i] ]
                break
            # If the current symbol is 'I' and the next symbol is 'V' or 'X', subtract the value of the current symbol from the value of the next symbol and add the result to the integer representation.
            # Increment i by 1 to skip the next symbol.
            if s[i] == 'I' and  s[i+1] in ('V', 'X'):
                roman_numeral_as_integer += ( roman_numerals[ s[i+1] ] - roman_numerals[ s[i] ] )
                i += 1
            # If the current symbol is 'X' and the next symbol is 'L' or 'C', subtract the value of the current symbol from the value of the next symbol and add the result to the integer representation.
            # Increment i by 1 to skip the next symbol.
            elif s[i] == 'X' and s[i+1] in ('L', 'C'):
                roman_numeral_as_integer += ( roman_numerals[ s[i+1] ] - roman_numerals[ s[i] ] )
                i += 1
            # If the current symbol is 'C' and the next symbol is 'D' or 'M', subtract the value of the current symbol from the value of the next symbol and add the result to the integer representation.
            # Increment i by 1 to skip the next symbol.
            elif s[i] == 'C' and s[i+1] in ('D', 'M'):
                roman_numeral_as_integer += ( roman_numerals[ s[i+1] ] - roman_numerals[ s[i] ] )
                i += 1
            # Otherwise, add the value of the current symbol to the integer representation.
            else:
                roman_numeral_as_integer += roman_numerals[ s[i] ]
            # Increment i by 1 to move to the next symbol.
            i += 1
        # Return the final integer representation of the Roman numeral string.
        return roman_numeral_as_integer


import os
from collections import defaultdict

class DecisionTreeNode:
    def __init__(self, f, t, i, tree, is_categorical=False):
        self.is_leaf = False
        self.threshold = t
        self.feature = f
        self.left = None
        self.right = None
        self.id = i
        self.tree = tree
        self.is_categorical = is_categorical
        self.parent = None
        self.count_left = 0
        self.count_right = 0

    def __ge__(self, other):
        return self.id >= other.id

    def __gt__(self, other):
        return self.id > other.id

    def __le__(self, other):
        return self.id <= other.id

    def __lt__(self, other):
        return self.id < other.id

    def _decide(self, e):
        c_val = e.features[self.feature]
        if c_val == "?" and len(e.instance.domains[self.feature]) > 0:
            c_val = e.instance.domains_max[self.feature]

        if c_val == "?":
            if self.count_right > self.count_left:
                return self.right
            else:
                return self.left
        if (self.is_categorical and c_val == self.threshold) \
                or (not self.is_categorical and c_val <= self.threshold):
            return self.left
        else:
            return self.right

    def train(self, samples):
        ls = []
        rs = []

        for e in samples:
            target = self._decide(e)
            if target.id == self.left.id:
                ls.append(e)
            else:
                rs.append(e)
        self.count_left = len(ls)
        self.count_right = len(rs)
        self.left.train(ls)
        self.right.train(rs)

    def decide(self, e):
        return self._decide(e).decide(e)

    def decide_length(self, e):
        return self._decide(e).decide_length(e) + 1

    def get_depth(self):
        return max(self.left.get_depth(), self.right.get_depth()) + 1

    def get_nodes(self):
        return self.left.get_nodes() + self.right.get_nodes() + 1

    def remap(self, mapping):
        if self.feature in mapping:
            self.feature = mapping[self.feature]
        self.left.remap(mapping)
        self.right.remap(mapping)

    def get_children(self):
        return [self.left, self.right]

    def get_path(self, e):
        pth = self._decide(e).get_path(e)
        pth.append(self)
        return pth

    def get_leaves(self):
        return self.left.get_leaves() + self.right.get_leaves()

    def get_extended_leaves(self):
        return self.left.get_extended_leaves() + self.right.get_extended_leaves()

    def reclassify(self, samples):
        ls = []
        rs = []

        for e in samples:
            target = self._decide(e)
            if target.id == self.left.id:
                ls.append(e)
            else:
                rs.append(e)

        self.left.reclassify(ls)
        self.right.reclassify(rs)

    def copy(self, new_tree, c_p, is_left):
        if c_p is None:
            n_n = new_tree.set_root(self.feature, self.threshold, self.is_categorical)
        else:
            n_n = new_tree.add_node(self.feature, self.threshold, c_p.id, is_left, self.is_categorical)
        self.left.copy(new_tree, n_n, True)
        self.right.copy(new_tree, n_n, False)

class DecisionTreeLeaf:
    def __init__(self, c, i, tree):
        self.is_leaf = True
        self.cls = c
        self.id = i
        self.tree = tree
        self.parent = None
        self.count = 0

    def __ge__(self, other):
        return self.id >= other.id

    def __gt__(self, other):
        return self.id > other.id

    def __le__(self, other):
        return self.id <= other.id

    def __lt__(self, other):
        return self.id < other.id

    def decide(self, e):
        return self.cls, self

    def train(self, samples):
        self.count = len(samples)

    def get_depth(self):
        return 0

    def get_nodes(self):
        return 1

    def get_leaves(self):
        return 1

    def decide_length(self, e):
        return 0

    def remap(self, mapping):
        return

    def get_path(self, e):
        return [self]

    def get_extended_leaves(self):
        return 0 if self.cls.startswith("-") or self.cls == "EmptyLeaf" else 1

    def reclassify(self, samples):
        classes = defaultdict(int)
        for cs in samples:
            classes[cs.cls] += 1
        if len(samples) > 0:
            _, self.cls = max((v, k) for k, v in classes.items())

    def copy(self, new_tree, c_p, is_left):
        if c_p is None:
            new_tree.set_root_leaf(self.cls)
        else:
            new_tree.add_leaf(self.cls, c_p.id, is_left)

class DecisionTree:
    def __init__(self):
        self.root = None
        self.nodes = [None]
        self.c_idx = 1

    def set_root_leaf(self, c):
        self.root = DecisionTreeLeaf(c, 1, self)
        self.nodes.append(self.root)
        return self.root

    def set_root(self, f, t, is_categorical=False):
        self.root = DecisionTreeNode(f, t, 1, self, is_categorical)
        self.nodes.append(self.root)
        return self.root

    def train(self, instance):
        self.root.train(instance.examples)

    def _get_id(self):
        while self.c_idx < len(self.nodes) and self.nodes[self.c_idx]:
            self.c_idx += 1
        if self.c_idx >= len(self.nodes):
            self.nodes.append(None)
        return self.c_idx

    def add_node(self, f, t, parent_id, is_left, is_categorical=False):
        c_id = self._get_id()

        new_node = DecisionTreeNode(f, t, c_id, self, is_categorical)
        new_node.parent = self.nodes[parent_id]

        if is_left:
            self.nodes[parent_id].left = new_node
        else:
            self.nodes[parent_id].right = new_node

        self.nodes[c_id] = new_node
        return new_node

    def add_leaf(self, c, parent_id, is_left):
        c_id = self._get_id()
        new_leaf = DecisionTreeLeaf(c, c_id, self)
        new_leaf.parent = self.nodes[parent_id]

        if is_left:
            self.nodes[parent_id].left = new_leaf
        else:
            self.nodes[parent_id].right = new_leaf
        self.nodes[c_id] = new_leaf
        return new_leaf

    def get_accuracy(self, examples):
        errors = 0
        for c_e in examples:
            decision = self.root.decide(c_e)[0]
            if decision != c_e.cls and (c_e.surrogate_cls is None or c_e.surrogate_cls != decision):
                errors += 1

        return (len(examples) - errors) / len(examples)

    def get_avg_length(self, examples):
        lengths = 0
        for c_e in examples:
            lengths += self.root.decide_length(c_e)

        return lengths / len(examples)

    def get_depth(self):
        return self.root.get_depth()

    def get_nodes(self):
        return self.root.get_nodes()

    def as_string(self):
        lines = []

        def add_node(n, indent):
            n_id = f"c({n.cls})" if n.is_leaf else \
                f"a({n.feature}){' =' if n.is_categorical else ' <='} {n.threshold}"
            lines.append(indent + "" + n_id)
            if not n.is_leaf:
                add_node(n.left, indent+"-")
                add_node(n.right, indent+"-")

        add_node(self.root, "")
        return os.linesep.join(lines)

    def assign(self, instance):
        assigned = defaultdict(list)
        for e in instance.examples:
            if e.ignore:
                continue

            pth = self.root.get_path(e)
            for c_node in pth:
                assigned[c_node.id].append(e)
        return assigned

    def copy(self):
        n_tree = DecisionTree()
        self.root.copy(n_tree, None, None)
        return n_tree

    def clean(self, instance, min_samples=1):
        assigned = self.assign(instance)

        def clean_sub(node):
            if node.is_leaf:
                return
            clean_sub(node.left)
            clean_sub(node.right)

            # Case one, two few examples
            replace_left = node.left.is_leaf and (node.left.id not in assigned or len(assigned[node.left.id]) < min_samples)
            # Case two, both leaves have the same class
            replace_left = replace_left or (node.left.is_leaf and node.right.is_leaf and node.left.cls == node.right.cls)
            replace_left = replace_left or (node.left.is_leaf and node.left.cls == "EmptyLeaf")
            replace_right = node.right.is_leaf and (node.right.id not in assigned or len(assigned[node.right.id]) < min_samples)
            replace_right = replace_right or (node.right.is_leaf and node.right.cls == "EmptyLeaf")

            if replace_left or replace_right:
                if node.parent is not None:
                    self.c_idx = 1
                    is_left = node.parent.left.id == node.id

                    self.nodes[node.id] = None
                    if replace_left:
                        self.nodes[node.left.id] = None
                        node.right.parent = node.parent
                        assigned[node.right.id] = assigned[node.id]
                        if is_left:
                            node.parent.left = node.right
                        else:
                            node.parent.right = node.right
                    elif replace_right:
                        self.nodes[node.right.id] = None
                        node.left.parent = node.parent
                        assigned[node.left.id] = assigned[node.id]
                        if is_left:
                            node.parent.left = node.left
                        else:
                            node.parent.right = node.left
                # Special case root
                else:
                    self.c_idx = 1
                    if replace_left:
                        self.nodes[node.id] = self.nodes[node.right.id]
                        self.nodes[node.right.id] = None
                        self.nodes[node.left.id] = None
                        node.right.id = node.id
                        node.right.parent = None
                        assigned[node.right.id] = assigned[node.id]
                        self.root = node.right
                    elif replace_right:
                        self.nodes[node.id] = self.nodes[node.left.id]
                        self.nodes[node.left.id] = None
                        self.nodes[node.right.id] = None
                        node.left.id = node.id
                        node.left.parent = None
                        assigned[node.left.id] = assigned[node.id]
                        self.root = node.left

        clean_sub(self.root)

    def check(self):
        """Perform a sanity check."""
        q = [self.root]
        nodes = set()

        while q:
            c_n = q.pop()
            if c_n.id in nodes:
                print("Duplicate node id or loop")
            nodes.add(c_n.id)

            if self.nodes[c_n.id] != c_n:
                print("Node id doesn't match node")

            if not c_n.is_leaf:
                q.extend(c_n.get_children())
                if c_n.left is None or c_n.right is None:
                    print("Child is none")

            if c_n.parent is not None and not (c_n.parent.left.id == c_n.id or c_n.parent.right.id == c_n.id):
                print("Invalid parent/child relationship")

            if c_n.parent is None and c_n != self.root:
                print("Double root")

        if any(x.id not in nodes for x in self.nodes if x is not None):
            print("Superfluous nodes")

def tree_compare(n1, n2):
    if n1.is_leaf != n2.is_leaf:
        raise RuntimeError("Leaf non leaf.")

    if not n1.is_leaf:
        if n1.feature != n2.feature:
            raise RuntimeError("Features diverge.")
        if n1.threshold != n2.threshold:
            raise RuntimeError("Threshold diverge.")
        if n1.is_categorical != n2.is_categorical:
            raise RuntimeError("Categorical diverge.")
        tree_compare(n1.left, n2.left)
        tree_compare(n1.right, n2.right)
    elif n1.cls != n2.cls:
        raise RuntimeError("Divergent classes.")


# -*- coding: utf-8 -*-
"""
Created on Mon Aug 17 13:27:26 2020
@author: DiegoZ

Traducir un mensaje en código Morse a letras del alfabeto segpun la tabla de
traducción.

"""

#este diccionario lo utilizo para traducir de morse a letras delalfabeto.
diccionario_morse = {
     '.-':'A',
     '-...':'B',
     '-.-.':'C',
     '-..':'D',
     '.':'E',
     '..-.':'F',
     '--.':'G',
     '....':'H',
     '..':'I',
     '.---':'J',
     '-.-':'K',
     '.-..':'L',
     '--':'M',
     '-.':'N',
     '---':'O',
     '.--.':'P',
     '--.-':'Q',
     '.-.':'R',
     '...':'S',
     '-':'T',
     '..-':'U',
     '...-':'V',
     '.--':'W',
     '-..-':'X',
     '-.--':'Y',
     '--..':'Z',
     '-----':'0',
     '.----':'1',
     '..---':'2',
     '...--':'3',
     '....-':'4',
     '.....':'5',
     '-....':'6',
     '--...':'7',
     '---..':'8',
     '----.':'9',
     '.-.-.-':''  #'<fin>'
     }

#este diccionario lo utilizo para transformar de letras a morse
diccionario_letras_a_morse = {}

def armar_diccionario_letras() -> {}:
    """
    Doy vuelta el diccionario Morse para que sea más facil traducir de letras
    a morse.
    Lo único que hay que hacer es llamar a la función,y si nunca cargó el
    diccionario inverso, lo va a hacer.
    """
    if len(diccionario_letras_a_morse) == 0:
        claves = diccionario_morse.keys()
        for k in claves:
            diccionario_letras_a_morse[ diccionario_morse[k] ] = k
    return diccionario_letras_a_morse

def translate_2_human(morse_str : str) -> str:
    """ Traducir un string que contien simbolos en código morse.
    Parametros:
        morse_str : string para taducir
    Retorna:
        un string que se puede leer fácilmente.
    """
    traduccion = []
    morse_array = morse_str.split(' ')
    for m in morse_array:
        if m in diccionario_morse:
            x = diccionario_morse[m]
        else :
            x = ' '
        traduccion.append(x)
    return ''.join(traduccion)

def translate_2_morse(human_readable_str : str, incluir_fullstop : bool = True) -> str:
    """ Traducir un mensaje de texto a a código morse. Solo soporta las letras
    que actualmente tenemos en la tabla y las palabras van separadas por
    espacios.

    Parametros:
        human_readable_str : string para convertir en codigo morse
    Retorna:
        un string en codigo morse.
    """
    diccionario = armar_diccionario_letras()
    lista_codigo_morse=[]
    palabras = human_readable_str.split(' ')
    for palabra in palabras:
        for letra in palabra:
            if (not (letra == None)) and letra.upper() in diccionario:
                lista_codigo_morse.append( diccionario[ letra.upper() ] )
            else:
                lista_codigo_morse.append( letra )

    if incluir_fullstop:
        lista_codigo_morse.append( diccionario[''] )

    return ' '.join( lista_codigo_morse )


list_of_strings = []
sorted_strings = []
add_another_word = True
not_anagram = False

while add_another_word:
    list_of_strings.append(input(f'''Please enter a word to check if it is an anagram with other words.
> ''').replace(" ","").lower()) # Get user input, replace " " with "" (remove white space). Change input to lowercase.

    if list_of_strings[len(list_of_strings)-1] == "!quit": # Check if user typed "!quit"
        list_of_strings.remove("!quit") # Remove "!quit" as it is not intended to be checked for anagram.
        for ana_string in list_of_strings: # Move through the list of strings.
            ana_string = list(ana_string) # Convert string to a list.
            ana_string.sort() # Sort the list by alphabetical order.
            sorted_strings.append(''.join(ana_string)) # Convert list back to string.

        for check_string in range(len(sorted_strings)): # Check each sorted string.
            if not sorted_strings[check_string] == sorted_strings[check_string-1]: # If current string being checked does not match previous string then not anagram.
                print(f'''Your words {list_of_strings} sorted as {sorted_strings} are not an anagram. Sorry.''') # Tell user result.
                not_anagram = True # Set variable for later.
                break # Break check, not anagram, no use checking the rest.

        if not not_anagram: # If not_anagram is flagged True, then not not anagram, is an anagram.
            print(f'''Your words {list_of_strings} sorted as {sorted_strings} are an anagram. Congrats!''') # Tell user result.
        print("goodbye") # Tell user goodbye.
        add_another_word = False # End loop.