import datetime
from math import cos, sin, tan, acos, asin, atan2, floor, ceil
from math import radians, degrees, pow

try:
    import pytz
except ImportError:
    raise ImportError(('The astral module requires the '
        'pytz module to be available.'))

__all__ = ['City','Astral','AstralError']

__version__ = "0.6"
__author__ = "Simon Kennedy <code@sffjunkie.co.uk>"

_CITY_INFO = """Abu Dhabi,UAE,24°28'N,54°22'E,Asia/Dubai
Abu Dhabi,United Arab Emirates,24°28'N,54°22'E,Asia/Dubai
Abuja,Nigeria,09°05'N,07°32'E,Africa/Lagos
Accra,Ghana,05°35'N,00°06'W,Africa/Accra
Addis Ababa,Ethiopia,09°02'N,38°42'E,Africa/Addis_Ababa
Adelaide,Australia,34°56'S,138°36'E,Australia/Adelaide
Al Jubail,Saudi Arabia,25°24'N,49°39'W,Asia/Riyadh
Algiers,Algeria,36°42'N,03°08'E,Africa/Algiers
Amman,Jordan,31°57'N,35°52'E,Asia/Amman
Amsterdam,Netherlands,52°23'N,04°54'E,Europe/Amsterdam
Andorra la Vella,Andorra,42°31'N,01°32'E,Europe/Andorra
Ankara,Turkey,39°57'N,32°54'E,Europe/Istanbul
Antananarivo,Madagascar,18°55'S,47°31'E,Indian/Antananarivo
Apia,Samoa,13°50'S,171°50'W,Pacific/Apia
Ashgabat,Turkmenistan,38°00'N,57°50'E,Asia/Ashgabat
Asmara,Eritrea,15°19'N,38°55'E,Africa/Asmara
Astana,Kazakhstan,51°10'N,71°30'E,Asia/Qyzylorda
Asuncion,Paraguay,25°10'S,57°30'W,America/Asuncion
Athens,Greece,37°58'N,23°46'E,Europe/Athens
Baghdad,Iraq,33°20'N,44°30'E,Asia/Baghdad
Baku,Azerbaijan,40°29'N,49°56'E,Asia/Baku
Bamako,Mali,12°34'N,07°55'W,Africa/Bamako
Bandar Seri Begawan,Brunei Darussalam,04°52'N,115°00'E,Asia/Brunei
Bangkok,Thailand,13°45'N,100°35'E,Asia/Bangkok
Bangui,Central African Republic,04°23'N,18°35'E,Africa/Bangui
Banjul,Gambia,13°28'N,16°40'W,Africa/Banjul
Basse-Terre,Guadeloupe,16°00'N,61°44'W,America/Guadeloupe
Basseterre,Saint Kitts and Nevis,17°17'N,62°43'W,America/St_Kitts
Beijing,China,39°55'N,116°20'E,Asia/Harbin
Beirut,Lebanon,33°53'N,35°31'E,Asia/Beirut
Belgrade,Yugoslavia,44°50'N,20°37'E,Europe/Belgrade
Belmopan,Belize,17°18'N,88°30'W,America/Belize
Berlin,Germany,52°30'N,13°25'E,Europe/Berlin
Bern,Switzerland,46°57'N,07°28'E,Europe/Zurich
Bishkek,Kyrgyzstan,42°54'N,74°46'E,Asia/Bishkek
Bissau,Guinea-Bissau,11°45'N,15°45'W,Africa/Bissau
Bloemfontein,South Africa,29°12'S,26°07'E,Africa/Johannesburg
Bogota,Colombia,04°34'N,74°00'W,America/Bogota
Brasilia,Brazil,15°47'S,47°55'W,Brazil/East
Bratislava,Slovakia,48°10'N,17°07'E,Europe/Bratislava
Brazzaville,Congo,04°09'S,15°12'E,Africa/Brazzaville
Bridgetown,Barbados,13°05'N,59°30'W,America/Barbados
Brisbane,Australia,27°30'S,153°01'E,Australia/Brisbane
Brussels,Belgium,50°51'N,04°21'E,Europe/Brussels
Bucharest,Romania,44°27'N,26°10'E,Europe/Bucharest
Bucuresti,Romania,44°27'N,26°10'E,Europe/Bucharest
Budapest,Hungary,47°29'N,19°05'E,Europe/Budapest
Buenos Aires,Argentina,36°30'S,60°00'W,America/Argentina/Buenos_Aires
Bujumbura,Burundi,03°16'S,29°18'E,Africa/Bujumbura
Cairo,Egypt,30°01'N,31°14'E,Africa/Cairo
Canberra,Australia,35°15'S,149°08'E,Australia/Canberra
Cape Town,South Africa,33°55'S,18°22'E,Africa/Johannesburg
Caracas,Venezuela,10°30'N,66°55'W,America/Caracas
Castries,Saint Lucia,14°02'N,60°58'W,America/St_Lucia
Cayenne,French Guiana,05°05'N,52°18'W,America/Cayenne
Charlotte Amalie,United States of Virgin Islands,18°21'N,64°56'W,America/Virgin
Chisinau,Moldova,47°02'N,28°50'E,Europe/Chisinau
Conakry,Guinea,09°29'N,13°49'W,Africa/Conakry
Copenhagen,Denmark,55°41'N,12°34'E,Europe/Copenhagen
Cotonou,Benin,06°23'N,02°42'E,Africa/Porto-Novo
Dakar,Senegal,14°34'N,17°29'W,Africa/Dakar
Damascus,Syrian Arab Republic,33°30'N,36°18'E,Asia/Damascus
Dammam,Saudi Arabia,26°30'N,50°12'E,Asia/Riyadh
Dhaka,Bangladesh,23°43'N,90°26'E,Asia/Dhaka
Dili,East Timor,08°29'S,125°34'E,Asia/Dili
Djibouti,Djibouti,11°08'N,42°20'E,Africa/Djibouti
Dodoma,United Republic of Tanzania,06°08'S,35°45'E,Africa/Dar_es_Salaam
Doha,Qatar,25°15'N,51°35'E,Asia/Qatar
Dublin,Ireland,53°21'N,06°15'W,Europe/Dublin
Dushanbe,Tajikistan,38°33'N,68°48'E,Asia/Dushanbe
Fort-de-France,Martinique,14°36'N,61°02'W,America/Martinique
Freetown,Sierra Leone,08°30'N,13°17'W,Africa/Freetown
Funafuti,Tuvalu,08°31'S,179°13'E,Pacific/Funafuti
Gaborone,Botswana,24°45'S,25°57'E,Africa/Gaborone
George Town,Cayman Islands,19°20'N,81°24'W,America/Cayman
Georgetown,Guyana,06°50'N,58°12'W,America/Guyana
Guatemala,Guatemala,14°40'N,90°22'W,America/Guatemala
Hanoi,Viet Nam,21°05'N,105°55'E,Asia/Saigon
Harare,Zimbabwe,17°43'S,31°02'E,Africa/Harare
Havana,Cuba,23°08'N,82°22'W,America/Havana
Helsinki,Finland,60°15'N,25°03'E,Europe/Helsinki
Hobart,Tasmania,42°53'S,147°19'E,Australia/Hobart
Honiara,Solomon Islands,09°27'S,159°57'E,Pacific/Guadalcanal
Islamabad,Pakistan,33°40'N,73°10'E,Asia/Karachi
Jakarta,Indonesia,06°09'S,106°49'E,Asia/Jakarta
Jerusalem,Israel,31°47'N,35°12'E,Asia/Jerusalem
Jubail,Saudi Arabia,27°02'N,49°39'E,Asia/Riyadh
Kabul,Afghanistan,34°28'N,69°11'E,Asia/Kabul
Kampala,Uganda,00°20'N,32°30'E,Africa/Kampala
Kathmandu,Nepal,27°45'N,85°20'E,Asia/Kathmandu
Khartoum,Sudan,15°31'N,32°35'E,Africa/Khartoum
Kiev,Ukraine,50°30'N,30°28'E,Europe/Kiev
Kigali,Rwanda,01°59'S,30°04'E,Africa/Kigali
Kingston,Jamaica,18°00'N,76°50'W,America/Jamaica
Kingston,Norfolk Island,45°20'S,168°43'E,Pacific/Norfolk
Kingstown,Saint Vincent and the Grenadines,13°10'N,61°10'W,America/St_Vincent
Kinshasa,Democratic Republic of the Congo,04°20'S,15°15'E,Africa/Kinshasa
Koror,Palau,07°20'N,134°28'E,Pacific/Palau
Kuala Lumpur,Malaysia,03°09'N,101°41'E,Asia/Kuala_Lumpur
Kuwait,Kuwait,29°30'N,48°00'E,Asia/Kuwait
La Paz,Bolivia,16°20'S,68°10'W,America/La_Paz
Libreville,Gabon,00°25'N,09°26'E,Africa/Libreville
Lilongwe,Malawi,14°00'S,33°48'E,Africa/Blantyre
Lima,Peru,12°00'S,77°00'W,America/Lima
Lisbon,Portugal,38°42'N,09°10'W,Europe/Lisbon
Ljubljana,Slovenia,46°04'N,14°33'E,Europe/Ljubljana
Lome,Togo,06°09'N,01°20'E,Africa/Lome
London,England,51°30'N,00°07'W,Europe/London
Luanda,Angola,08°50'S,13°15'E,Africa/Luanda
Lusaka,Zambia,15°28'S,28°16'E,Africa/Lusaka
Luxembourg,Luxembourg,49°37'N,06°09'E,Europe/Luxembourg
Macau,Macao,22°12'N,113°33'E,Asia/Macau
Madinah,Saudi Arabia,24°28'N,39°36'E,Asia/Riyadh
Madrid,Spain,40°25'N,03°45'W,Europe/Madrid
Makkah,Saudi Arabia,21°26'N,39°49'E,Asia/Riyadh
Malabo,Equatorial Guinea,03°45'N,08°50'E,Africa/Malabo
Male,Maldives,04°00'N,73°28'E,Indian/Maldives
Mamoudzou,Mayotte,12°48'S,45°14'E,Indian/Mayotte
Managua,Nicaragua,12°06'N,86°20'W,America/Managua
Manama,Bahrain,26°10'N,50°30'E,Asia/Bahrain
Manila,Philippines,14°40'N,121°03'E,Asia/Manila
Maputo,Mozambique,25°58'S,32°32'E,Africa/Maputo
Maseru,Lesotho,29°18'S,27°30'E,Africa/Maseru
Masqat,Oman,23°37'N,58°36'E,Asia/Muscat
Mbabane,Swaziland,26°18'S,31°06'E,Africa/Mbabane
Mecca,Saudi Arabia,21°26'N,39°49'E,Asia/Riyadh
Medina,Saudi Arabia,24°28'N,39°36'E,Asia/Riyadh
Mexico,Mexico,19°20'N,99°10'W,America/Mexico_City
Minsk,Belarus,53°52'N,27°30'E,Europe/Minsk
Mogadishu,Somalia,02°02'N,45°25'E,Africa/Mogadishu
Monrovia,Liberia,06°18'N,10°47'W,Africa/Monrovia
Montevideo,Uruguay,34°50'S,56°11'W,America/Montevideo
Moroni,Comoros,11°40'S,43°16'E,Indian/Comoro
Moscow,Russian Federation,55°45'N,37°35'E,Europe/Moscow
Moskva,Russian Federation,55°45'N,37°35'E,Europe/Moscow
Mumbai,India,18°58'N,72°49'E,Asia/Kolkata
N'Djamena,Chad,12°10'N,14°59'E,Africa/Ndjamena
Nairobi,Kenya,01°17'S,36°48'E,Africa/Nairobi
Nassau,Bahamas,25°05'N,77°20'W,America/Nassau
New Delhi,India,28°37'N,77°13'E,Asia/Kolkata
Niamey,Niger,13°27'N,02°06'E,Africa/Niamey
Nicosia,Cyprus,35°10'N,33°25'E,Asia/Nicosia
Nouakchott,Mauritania,20°10'S,57°30'E,Africa/Nouakchott
Noumea,New Caledonia,22°17'S,166°30'E,Pacific/Noumea
Nuku'alofa,Tonga,21°10'S,174°00'W,Pacific/Tongatapu
Nuuk,Greenland,64°10'N,51°35'W,America/Godthab
Oranjestad,Aruba,12°32'N,70°02'W,America/Aruba
Oslo,Norway,59°55'N,10°45'E,Europe/Oslo
Ottawa,Canada,45°27'N,75°42'W,US/Eastern
Ouagadougou,Burkina Faso,12°15'N,01°30'W,Africa/Ouagadougou
P'yongyang,Democratic People's Republic of Korea,39°09'N,125°30'E,Asia/Pyongyang
Pago Pago,American Samoa,14°16'S,170°43'W,Pacific/Pago_Pago
Palikir,Micronesia,06°55'N,158°09'E,Pacific/Ponape
Panama,Panama,09°00'N,79°25'W,America/Panama
Papeete,French Polynesia,17°32'S,149°34'W,Pacific/Tahiti
Paramaribo,Suriname,05°50'N,55°10'W,America/Paramaribo
Paris,France,48°50'N,02°20'E,Europe/Paris
Perth,Australia,31°56'S,115°50'E,Australia/Perth
Phnom Penh,Cambodia,11°33'N,104°55'E,Asia/Phnom_Penh
Port Moresby,Papua New Guinea,09°24'S,147°08'E,Pacific/Port_Moresby
Port-Vila,Vanuatu,17°45'S,168°18'E,Pacific/Efate
Port-au-Prince,Haiti,18°40'N,72°20'W,America/Port-au-Prince
Porto-Novo,Benin,06°23'N,02°42'E,Africa/Porto-Novo
Prague,Czech Republic,50°05'N,14°22'E,Europe/Prague
Praia,Cape Verde,15°02'N,23°34'W,Atlantic/Cape_Verde
Pretoria,South Africa,25°44'S,28°12'E,Africa/Johannesburg
Quito,Ecuador,00°15'S,78°35'W,America/Guayaquil
Reykjavik,Iceland,64°10'N,21°57'W,Atlantic/Reykjavik
Riga,Latvia,56°53'N,24°08'E,Europe/Riga
Riyadh,Saudi Arabia,24°41'N,46°42'E,Asia/Riyadh
Road Town,British Virgin Islands,18°27'N,64°37'W,America/Virgin
Rome,Italy,41°54'N,12°29'E,Europe/Rome
Roseau,Dominica,15°20'N,61°24'W,America/Dominica
Saint Pierre,Saint Pierre and Miquelon,46°46'N,56°12'W,America/Miquelon
Saipan,Northern Mariana Islands,15°12'N,145°45'E,Pacific/Saipan
San Jose,Costa Rica,09°55'N,84°02'W,America/Costa_Rica
San Juan,Puerto Rico,18°28'N,66°07'W,America/Puerto_Rico
San Marino,San Marino,43°55'N,12°30'E,Europe/San_Marino
San Salvador,El Salvador,13°40'N,89°10'W,America/El_Salvador
Santiago,Chile,33°24'S,70°40'W,America/Santiago
Santo Domingo,Dominica Republic,18°30'N,69°59'W,America/Santo_Domingo
Sao Tome,Sao Tome and Principe,00°10'N,06°39'E,Africa/Sao_Tome
Sarajevo,Bosnia and Herzegovina,43°52'N,18°26'E,Europe/Sarajevo
Seoul,Republic of Korea,37°31'N,126°58'E,Asia/Seoul
Skopje,The Former Yugoslav Republic of Macedonia,42°01'N,21°26'E,Europe/Skopje
Sofia,Bulgaria,42°45'N,23°20'E,Europe/Sofia
St. Peter Port,Guernsey,49°26'N,02°33'W,Europe/Guernsey
Stanley,Falkland Islands,51°40'S,59°51'W,Atlantic/Stanley
Stockholm,Sweden,59°20'N,18°05'E,Europe/Stockholm
Sucre,Bolivia,16°20'S,68°10'W,America/La_Paz
Suva,Fiji,18°06'S,178°30'E,Pacific/Fiji
Sydney,Australia,33°53'S,151°13'E,Australia/Sydney
T'bilisi,Georgia,41°43'N,44°50'E,Asia/Tbilisi
Tallinn,Estonia,59°22'N,24°48'E,Europe/Tallinn
Tarawa,Kiribati,01°30'N,173°00'E,Pacific/Tarawa
Tashkent,Uzbekistan,41°20'N,69°10'E,Asia/Tashkent
Tegucigalpa,Honduras,14°05'N,87°14'W,America/Tegucigalpa
Tehran,Iran,35°44'N,51°30'E,Asia/Tehran
Thimphu,Bhutan,27°31'N,89°45'E,Asia/Thimphu
Tirana,Albania,41°18'N,19°49'E,Europe/Tirane
Tirane,Albania,41°18'N,19°49'E,Europe/Tirane
Torshavn,Faroe Islands,62°05'N,06°56'W,Atlantic/Faroe
Tokyo,Japan,35°41'N,139°41'E,Asia/Tokyo
Tripoli,Libyan Arab Jamahiriya,32°49'N,13°07'E,Africa/Tripoli
Tunis,Tunisia,36°50'N,10°11'E,Africa/Tunis
Vaduz,Liechtenstein,47°08'N,09°31'E,Europe/Vaduz
Valletta,Malta,35°54'N,14°31'E,Europe/Malta
Vienna,Austria,48°12'N,16°22'E,Europe/Vienna
Vientiane,Lao People's Democratic Republic,17°58'N,102°36'E,Asia/Vientiane
Vilnius,Lithuania,54°38'N,25°19'E,Europe/Vilnius
W. Indies,Antigua and Barbuda,17°20'N,61°48'W,America/Antigua
Warsaw,Poland,52°13'N,21°00'E,Europe/Warsaw
Washington DC,USA,39°91'N,77°02'W,US/Eastern
Wellington,New Zealand,41°19'S,174°46'E,Pacific/Auckland
Willemstad,Netherlands Antilles,12°05'N,69°00'W,America/Curacao
Windhoek,Namibia,22°35'S,17°04'E,Africa/Windhoek
Yamoussoukro,Cote d'Ivoire,06°49'N,05°17'W,Africa/Abidjan
Yangon,Myanmar,16°45'N,96°20'E,Asia/Rangoon
Yaounde,Cameroon,03°50'N,11°35'E,Africa/Douala
Yerevan,Armenia,40°10'N,44°31'E,Asia/Yerevan
Zagreb,Croatia,45°50'N,15°58'E,Europe/Zagreb

# UK Cities
Aberdeen,Scotland,57°08'N,02°06'W,Europe/London
Birmingham,England,52°30'N,01°50'W,Europe/London
Bolton,England,53°35'N,02°15'W,Europe/London
Bradford,England,53°47'N,01°45'W,Europe/London
Bristol,England,51°28'N,02°35'W,Europe/London
Cardiff,Wales,51°29'N,03°13'W,Europe/London
Crawley,England,51°8'N,00°10'W,Europe/London
Edinburgh,Scotland,55°57'N,03°13'W,Europe/London
Glasgow,Scotland,55°50'N,04°15'W,Europe/London
Greenwich,England,51°28'N,00°00'W,Europe/London
Leeds,England,53°48'N,01°35'W,Europe/London
Leicester,England,52°38'N,01°08'W,Europe/London
Liverpool,England,53°25'N,03°00'W,Europe/London
Manchester,England,53°30'N,02°15'W,Europe/London
Newcastle Upon Time,England,54°59'N,01°36'W,Europe/London
Newcastle,England,54°59'N,01°36'W,Europe/London
Norwich,England,52°38'N,01°18'E,Europe/London
Oxford,England,51°45'N,01°15'W,Europe/London
Plymouth,England,50°25'N,04°15'W,Europe/London
Portsmouth,England,50°48'N,01°05'W,Europe/London
Reading,England,51°27'N,0°58'W,Europe/London
Sheffield,England,53°23'N,01°28'W,Europe/London
Southampton,England,50°55'N,01°25'W,Europe/London
Swansea,England,51°37'N,03°57'W,Europe/London
Swindon,England,51°34'N,01°47'W,Europe/London
Wolverhampton,England,52°35'N,2°08'W,Europe/London

# US State Capitals
Montgomery,USA,32°21'N,86°16'W,US/Central
Juneau,USA,58°23'N,134°11'W,US/Alaska
Phoenix,USA,33°26'N,112°04'W,US/Mountain
Little Rock,USA,34°44'N,92°19'W,US/Central
Sacramento,USA,38°33'N,121°28'W,US/Pacific
Denver,USA,39°44'N,104°59'W,US/Mountain
Hartford,USA,41°45'N,72°41'W,US/Eastern
Dover,USA,39°09'N,75°31'W,US/Eastern
Tallahassee,USA,30°27'N,84°16'W,US/Eastern
Atlanta,USA,33°45'N,84°23'W,US/Eastern
Honolulu,USA,21°18'N,157°49'W,US/Hawaii
Boise,USA,43°36'N,116°12'W,US/Mountain
Springfield,USA,39°47'N,89°39'W,US/Central
Indianapolis,USA,39°46'N,86°9'W,US/Eastern
Des Moines,USA,41°35'N,93°37'W,US/Central
Topeka,USA,39°03'N,95°41'W,US/Central
Frankfort,USA,38°11'N,84°51'W,US/Eastern
Baton Rouge,USA,30°27'N,91°8'W,US/Central
Augusta,USA,44°18'N,69°46'W,US/Eastern
Annapolis,USA,38°58'N,76°30'W,US/Eastern
Boston,USA,42°21'N,71°03'W,US/Eastern
Lansing,USA,42°44'N,84°32'W,US/Eastern
Saint Paul,USA,44°56'N,93°05'W,US/Central
Jackson,USA,32°17'N,90°11'W,US/Central
Jefferson City,USA,38°34'N,92°10'W,US/Central
Helena,USA,46°35'N,112°1'W,US/Mountain
Lincoln,USA,40°48'N,96°40'W,US/Central
Carson City,USA,39°9'N,119°45'W,US/Pacific
Concord,USA,43°12'N,71°32'W,US/Eastern
Trenton,USA,40°13'N,74°45'W,US/Eastern
Santa Fe,USA,35°40'N,105°57'W,US/Mountain
Albany,USA,42°39'N,73°46'W,US/Eastern
Raleigh,USA,35°49'N,78°38'W,US/Eastern
Bismarck,USA,46°48'N,100°46'W,US/Central
Columbus,USA,39°59'N,82°59'W,US/Eastern
Oklahoma City,USA,35°28'N,97°32'W,US/Central
Salem,USA,44°55'N,123°1'W,US/Pacific
Harrisburg,USA,40°16'N,76°52'W,US/Eastern
Providence,USA,41°49'N,71°25'W,US/Eastern
Columbia,USA,34°00'N,81°02'W,US/Eastern
Pierre,USA,44°22'N,100°20'W,US/Central
Nashville,USA,36°10'N,86°47'W,US/Central
Austin,USA,30°16'N,97°45'W,US/Central
Salt Lake City,USA,40°45'N,111°53'W,US/Mountain
Montpelier,USA,44°15'N,72°34'W,US/Eastern
Richmond,USA,37°32'N,77°25'W,US/Eastern
Olympia,USA,47°2'N,122°53'W,US/Pacific
Charleston,USA,38°20'N,81°38'W,US/Eastern
Madison,USA,43°4'N,89°24'W,US/Central
Cheyenne,USA,41°8'N,104°48'W,US/Mountain

# Major US Cities
Birmingham,USA,33°39'N,86°48'W,US/Central
Anchorage,USA,61°13'N,149°53'W,US/Alaska
Los Angeles,USA,34°03'N,118°15'W,US/Pacific
San Francisco,USA,37°46'N,122°25'W,US/Pacific
Bridgeport,USA,41°11'N,73°11'W,US/Eastern
Wilmington,USA,39°44'N,75°32'W,US/Eastern
Jacksonville,USA,30°19'N,81°39'W,US/Eastern
Miami,USA,26°8'N,80°12'W,US/Eastern
Chicago,USA,41°50'N,87°41'W,US/Central
Wichita,USA,37°41'N,97°20'W,US/Central
Louisville,USA,38°15'N,85°45'W,US/Eastern
New Orleans,USA,29°57'N,90°4'W,US/Central
Portland,USA,43°39'N,70°16'W,US/Eastern
Baltimore,USA,9°17'N,76°37'W,US/Eastern
Detroit,USA,42°19'N,83°2'W,US/Eastern
Minneapolis,USA,44°58'N,93°15'W,US/Central
Kansas City,USA,39°06'N,94°35'W,US/Central
Billings,USA,45°47'N,108°32'W,US/Mountain
Omaha,USA,41°15'N,96°0'W,US/Central
Las Vegas,USA,36°10'N,115°08'W,US/Pacific
Manchester,USA,42°59'N,71°27'W,US/Eastern
Newark,USA,40°44'N,74°11'W,US/Eastern
Albuquerque,USA,35°06'N,106°36'W,US/Mountain
New York,USA,40°43'N,74°0'W,US/Eastern
Charlotte,USA,35°13'N,80°50'W,US/Eastern
Fargo,USA,46°52'N,96°47'W,US/Central
Cleveland,USA,41°28'N,81°40'W,US/Eastern
Portland,USA,45°31'N,122°40'W,US/Pacific
Philadelphia,USA,39°57'N,75°10'W,US/Eastern
Sioux Falls,USA,43°32'N,96°43'W,US/Central
Memphis,USA,35°07'N,89°58'W,US/Central
Houston,USA,29°45'N,95°22'W,US/Central
Dallas,USA,32°47'N,96°48'W,US/Central
Burlington,USA,44°28'N,73°9'W,US/Eastern
Virginia Beach,USA,36°50'N,76°05'W,US/Eastern
Seattle,USA,47°36'N,122°19'W,US/Pacific
Milwaukee,USA,43°03'N,87°57'W,US/Central
San Diego,USA,32°42'N,117°09'W,US/Pacific
Orlando,USA,28°32'N,81°22'W,US/Eastern
Buffalo,USA,42°54'N,78°50'W,US/Eastern
Toledo,USA,41°39'N,83°34'W,US/Eastern
"""

class AstralError(Exception):
    pass

class City(object):
    """Provides access to information for single city."""

    def __init__(self, info=None):
        """Initializes the object with a tuple of information.

        :param info: A tuple of information to fill in the city info.

            The tuple should contain items in the following order

            ================ =============
            Field            Default
            ================ =============
            name             Greenwich
            country          England
            latitude         51.168
            longitude        0
            time zone name   Europe/London
            ================ =============

            See :attr:`timezone` property for a method of obtaining time zone
            names
        """

        self.astral = None
        if info is None:
            self.name = 'Greenwich'
            self.country = 'England'
            self._latitude = 51.168
            self._longitude = 0
            self._timezone_group = 'Europe'
            self._timezone_location = 'London'
        else:
            self._latitude = 0
            self._longitude = 0
            self._timezone_group = ''
            self._timezone_location = ''

            try:
                self.name = info[0]
                self.country = info[1]
                self.latitude = info[2]
                self.longitude = info[3]
                self.timezone = info[4]
            except:
                pass

    def __repr__(self):
        return '%s/%s, tz=%s, lat=%0.02f, lon=%0.02f' % (self.name, self.country,
                                                  self.timezone,
                                                  self.latitude, self.longitude)

    def latitude():
        doc = """The city's latitude

        ``latitude`` can be set either as a string or as a number

        For strings they must be of the form::

            degrees°minutes'[N|S] e.g. 51°31'N

        For numbers, negative numbers signify latitudes to the South.
        """

        def fget(self):
            return self._latitude

        def fset(self, latitude):
            if isinstance(latitude, str):
                (deg, rest) = latitude.split("°", 1)
                (minute, rest) = rest.split("'", 1)

                self._latitude = float(deg) + (float(minute)/60)

                if latitude.endswith("S"):
                    self._latitude = -self._latitude
            else:
                self._latitude = float(latitude)

        return locals()

    latitude = property(**latitude())

    def longitude():
        doc = """The city's longitude.

        ``longitude`` can be set either as a string or as a number

        For strings they must be of the form::

            degrees°minutes'[E|W] e.g. 51°31'W

        For numbers, positive numbers signify longitudes to the East.
        """

        def fget(self):
            return self._longitude

        def fset(self, longitude):
            if isinstance(longitude, str):
                (deg, rest) = longitude.split("°", 1)
                (minute, rest) = rest.split("'", 1)

                self._longitude = float(deg) + (float(minute)/60)

                # Conventionally locations to the west of 0° are negative
                # but the calculation is based on the opposite
                if longitude.endswith("W"):
                    self._longitude = -self._longitude
            else:
                self._longitude = float(longitude)

        return locals()

    longitude = property(**longitude())

    def timezone():
        doc = """The name of the time zone in which the city is located.

        A list of time zone names can be obtained from pytz. For example.

        >>> from pytz import all_timezones
        >>> for tz in all_timezones:
        ...     print tz
        """

        def fget(self):
            if self._timezone_location != '':
                return '%s/%s' % (self._timezone_group, self._timezone_location)
            else:
                return self._timezone_group

        def fset(self, name):
            if name not in pytz.all_timezones:
                raise ValueError('Timezone \'%s\' not recognized' % name)

            try:
                self._timezone_group, self._timezone_location = \
                    name.split('/', 1)
            except ValueError:
                self._timezone_group = name
                self._timezone_location = ''

        return locals()

    timezone = property(**timezone())

    def tz():
        doc = """The timezone."""

        def fget(self):
            try:
                tz =  pytz.timezone(self.timezone)
                return tz
            except pytz.UnknownTimeZoneError:
                raise AstralError('Unknown timezone \'%s\'' % self.timezone)

        return locals()

    tz = property(**tz())

    def solar_depression():
        doc = """The number of degrees the sun must be below the horizon for the
        dawn/dusk calc.

        Can either be set as a number of degrees below the horizon or as
        one of the following strings

        ============= =======
        String        Degrees
        ============= =======
        civil            6.0
        nautical        12.0
        astronomical    18.0
        ============= =======
        """

        def fget(self):
            return self.astral.solar_depression

        def fset(self, depression):
            if self.astral is None:
                self.astral = Astral()

            self.astral.solar_depression = depression

        return locals()

    solar_depression = property(**solar_depression())

    def dawn(self, date=None, local=True):
        """Calculates the time in the morning when the sun is a certain number
        of degrees below the horizon. By default this is 6 degrees but can be
        changed by setting the :attr:`Astral.solar_depression` property.

        :param date: The date for which to calculate the dawn time.
                     A value of None uses the current date.

        :param local: True  = Time to be returned in city's time zone (Default);
                      False = Time to be returned in UTC.

        :rtype: :class:`datetime.datetime` of dawn
        """

        if self.astral is None:
            self.astral = Astral()

        if date is None:
            date = datetime.date.today()

        dawn = self.astral.dawn_utc(date, self.latitude, self.longitude)

        if local:
            return dawn.astimezone(self.tz)
        else:
            return dawn

    def sunrise(self, date=None, local=True):
        """Return sunrise time.

        Calculates the time in the morning when the sun is a 0.833 degrees
        below the horizon. This is to account for refraction.

        :param date: The date for which to calculate the sunrise time.
                     A value of None uses the current date.

        :param local: True  = Time to be returned in city's time zone (Default);
                      False = Time to be returned in UTC.

        :rtype: :class:`datetime.datetime` of sunrise
        """

        if self.astral is None:
            self.astral = Astral()

        if date is None:
            date = datetime.date.today()

        sunrise = self.astral.sunrise_utc(date, self.latitude, self.longitude)

        if local:
            return sunrise.astimezone(self.tz)
        else:
            return sunrise

    def solar_noon(self, date=None, local=True):
        """Calculates the solar noon (the time when the sun is at its highest
        point.)

        :param date: The date for which to calculate the noon time.
                     A value of None uses the current date.

        :param local: True  = Time to be returned in city's time zone (Default);
                      False = Time to be returned in UTC.

        :rtype: :class:`datetime.datetime` of noon
        """

        if self.astral is None:
            self.astral = Astral()

        if date is None:
            date = datetime.date.today()

        noon = self.astral.solar_noon_utc(date, self.longitude)

        if local:
            return noon.astimezone(self.tz)
        else:
            return noon

    def sunset(self, date=None, local=True):
        """Calculates sunset time (the time in the evening when the sun is a
        0.833 degrees below the horizon. This is to account for refraction.)

        :param date: The date for which to calculate the sunset time.
                     A value of None uses the current date.

        :param local: True  = Time to be returned in city's time zone (Default);
                      False = Time to be returned in UTC.

        :rtype: :class:`datetime.datetime` of sunset
        """

        if self.astral is None:
            self.astral = Astral()

        if date is None:
            date = datetime.date.today()

        sunset = self.astral.sunset_utc(date, self.latitude, self.longitude)

        if local:
            return sunset.astimezone(self.tz)
        else:
            return sunset

    def dusk(self, date=None, local=True):
        """Calculates the dusk time (the time in the evening when the sun is a
        certain number of degrees below the horizon. By default this is 6
        degrees but can be changed by setting the
        :attr:`Astral.solar_depression` property.)

        :param date: The date for which to calculate the dusk time.
                     A value of None uses the current date.

        :param local: True  = Time to be returned in city's time zone (Default);
                      False = Time to be returned in UTC.

        :rtype: :class:`datetime.datetime` of dusk
        """

        if self.astral is None:
            self.astral = Astral()

        if date is None:
            date = datetime.date.today()

        dusk = self.astral.dusk_utc(date, self.latitude, self.longitude)

        if local:
            return dusk.astimezone(self.tz)
        else:
            return dusk

    def sun(self, date=None, local=True):
        """Returns dawn, sunrise, noon, sunset and dusk as a dictionary.

        :param date: The date for which to calculate the times.
                     A value of None uses the current date.

        :param local: True  = Time to be returned in city's time zone (Default);
                      False = Time to be returned in UTC.

        :rtype: :class:`datetime.datetime` of dusk
        """

        if self.astral is None:
            self.astral = Astral()

        if date is None:
            date = datetime.date.today()

        sun = self.astral.sun_utc(date, self.latitude, self.longitude)

        if local:
            for key, dt in list(sun.items()):
                sun[key] = dt.astimezone(self.tz)

        return sun

    def rahukaalam(self, date=None, local=True):
        """Calculates the period of rahukaalam.

        :param date: The date for which to calculate the rahukaalam period.
                     A value of None uses the current date.

        :param local: True  = Time to be returned in city's time zone (Default);
                      False = Time to be returned in UTC.

        :rtype: :class:`datetime.datetime` of dusk
        """

        if self.astral is None:
            self.astral = Astral()

        if date is None:
            date = datetime.date.today()

        rahukaalam = self.astral.rahukaalam_utc(date,
            self.latitude, self.longitude)

        if local:
            for key, dt in list(rahukaalam.items()):
                rahukaalam[key] = dt.astimezone(self.tz)

        return rahukaalam

    def solar_azimuth(self, dateandtime=None):
        """Calculates the solar azimuth angle for a specific date/time.

        :param dateandtime: The date and time for which to calculate the angle.
        :type dateandtime: :class:`datetime.datetime`

        :rtype: The angle in degrees clockwise from North as a float.
        """

        if self.astral is None:
            self.astral = Astral()

        if dateandtime is None:
            dateandtime = datetime.datetime.now(tz=self.tz)

        return self.astral.solar_azimuth(dateandtime,
            self.latitude, self.longitude)

    def solar_elevation(self, dateandtime=None):
        """Calculates the solar elevation angle for a specific time.

        :param dateandtime: The date and time for which to calculate the angle.
        :type dateandtime: :class:`datetime.datetime`

        :rtype: The angle in degrees from the horizon as a float.
        """

        if self.astral is None:
            self.astral = Astral()

        if dateandtime is None:
            dateandtime = datetime.datetime.now(tz=self.tz)

        return self.astral.solar_elevation(dateandtime,
            self.latitude, self.longitude)

    def moon_phase(self, date=None):
        """Calculates the moon phase for a specific date.

        :param date: The date to calculate the phase for.
                     If ommitted the current date is used.
        :type date: datetime.date

        :rtype:
            Integer designating phase

                | 0  = New moon
                | 7  = First quarter
                | 14 = Full moon
                | 21 = Last quarter
        """

        if self.astral is None:
            self.astral = Astral()

        if date is None:
            date = datetime.date.today()

        return self.astral.moon_phase(date, self.tz)

class CityGroup(object):
    def __init__(self, name):
        self.name = name
        self._cities = {}

    def __getitem__(self, key):
        """Returns a City object for the specified city. ::

            group = astral.europe
            city = group['London']

        You can supply an optional country name by adding a comma
        followed by the country name. Where multiple cities have the
        same name you may need to supply the country name otherwise
        the first result will be returned which may not be the one
        you're looking for. ::

            city = group['Abu Dhabi,United Arab Emirates']

        Handles city names with spaces and mixed case.
        """

        name = str(key).lower().replace(' ', '_')

        try:
            city_name, country_name = name.split(',', 1)
        except:
            city_name = name
            country_name = ''

        city_name = city_name.strip('"\'')
        country_name = country_name.strip('"\'')

        for (name, city_list) in list(self._cities.items()):
            if name.replace(' ', '_') == city_name:
                if len(city_list) == 1 or country_name == '':
                    return city_list[0]

                for city in city_list:
                    if city.country.lower().replace(' ', '_') \
                    == country_name:
                        return city

        raise KeyError('Unrecognised city name - %s' % key)

    def __setitem__(self, key, value):
        key = str(key).lower()
        if key not in self._cities:
            self._cities[key] = [value]
        else:
            self._cities[key].append(value)

    def __contains__(self, key):
        key = str(key).lower()
        for name in list(self._cities.keys()):
            if name == key:
                return True

        return False

    def __iter__(self):
        for city_list in list(self._cities.values()):
            for city in city_list:
                yield city

    def keys(self):
        return list(self._cities.keys())

    def values(self):
        return list(self._cities.values())

    def items(self):
        return list(self._cities.items())

    def cities():
        def fget(self):
            k = []
            for city_list in list(self._cities.values()):
                for city in city_list:
                    k.append(city.name)

            return k

        return locals()

    cities = property(**cities())

class CityDB(object):
    def __init__(self):
        self._groups = {}

        cities = _CITY_INFO.split('\n')
        for line in cities:
            line = line.strip()
            if line != '' and line[0] != '#':
                if line[-1] == '\n':
                    line = line[:-1]

                info = line.split(',')

                city = City(info)

                timezone_group = city._timezone_group.lower()
                try:
                    group = self.__getattr__(timezone_group)
                except:
                    group = CityGroup(city._timezone_group)
                    self._groups[timezone_group] = group

                group[info[0].lower()] = city

    def __getattr__(self, key):
        """Access to each timezone group. For example London is in timezone
        group Europe.

        Attribute lookup is case insensitive"""

        key = str(key).lower()
        for name, value in list(self._groups.items()):
            if name == key:
                return value

        raise AttributeError('Group \'%s\' not found' % key)

    def __getitem__(self, key):
        """Lookup a city within all timezone groups.

        Item lookup is case insensitive."""

        key = str(key).lower()
        for group in list(self._groups.values()):
            try:
                return group[key]
            except KeyError:
                pass

        raise KeyError('Unrecognised city name - %s' % key)

    def __iter__(self):
        return self._groups.__iter__()

    def __contains__(self, key):
        key = str(key).lower()
        for name, group in list(self._groups.items()):
            if name == key:
                return True

            if key in group:
                return True

        return False

    def cities():
        def fget(self):
            k = []
            for group in list(self._groups.values()):
                k.extend(group.cities)

            return k

        return locals()

    cities = property(**cities())

    def groups():
        def fget(self):
            return self._groups

        return locals()

    groups = property(**groups())

class Astral(object):
    def __init__(self):
        """Initialise the city database and set the default depression."""

        self._citydb = CityDB()
        self._depression = 6  # Set default depression in degrees

    def __getitem__(self, key):
        """Returns the City instance specified by ``key``."""

        city = self._citydb[key]
        city.astral = self
        return city

    def citydb():
        doc = """:rtype: The database of cities."""

        def fget(self):
            return self._citydb

        return locals()

    citydb = property(**citydb())

    def solar_depression():
        doc = """The number of degrees the sun must be below the horizon for the
        dawn/dusk calc.

        Can either be set as a number of degrees below the horizon or as
        one of the following strings

        ============= =======
        String        Degrees
        ============= =======
        civil            6.0
        nautical        12.0
        astronomical    18.0
        ============= =======
        """

        def fget(self):
            return self._depression

        def fset(self, depression):
            if isinstance(depression, str):
                try:
                    self._depression = {
                        'civil': 6,
                        'nautical': 12,
                        'astronomical': 18}[depression]
                except:
                    raise KeyError(("solar_depression must be either a number "
                        "or one of 'civil', 'nautical' or 'astronomical'"))
            else:
                self._depression = float(depression)

        return locals()

    solar_depression = property(**solar_depression())

    def sun_utc(self, date, latitude, longitude):
        """Calculate all the info for the sun at once.

        :param date:       Date to calculate for.
        :type date:        :class:`datetime.date`
        :param latitude:   Latitude - Southern latitudes should be negative
        :type latitude:    float
        :param longitude:  Longitude as a floating point number. Eastern
                           latitudes should be negative
        :type longitude:   float

        :rtype:
            Dictionary with keys ``dawn``, ``sunrise``, ``noon``,
            ``sunset`` and ``dusk``
        """

        dawn = self.dawn_utc(date, latitude, longitude)
        sunrise = self.sunrise_utc(date, latitude, longitude)
        noon = self.solar_noon_utc(date, longitude)
        sunset = self.sunset_utc(date, latitude, longitude)
        dusk = self.dusk_utc(date, latitude, longitude)

        return {
            'dawn': dawn,
            'sunrise': sunrise,
            'noon': noon,
            'sunset': sunset,
            'dusk': dusk
        }

    def dawn_utc(self, date, latitude, longitude):
        """Calculate dawn time in the UTC timezone.

        :param date:       Date to calculate for.
        :type date:        datetime.date
        :param latitude:   Latitude - Southern latitudes should be negative
        :type latitude:    float
        :param longitude:  Longitude as a floating point number. Eastern
                           latitudes should be negative
        :type longitude:   float

        :rtype: date/time in UTC timezone
        """

        julianday = self._julianday(date)

        if latitude > 89.8:
            latitude = 89.8

        if latitude < -89.8:
            latitude = -89.8

        t = self._jday_to_jcentury(julianday)
        eqtime = self._eq_of_time(t)
        solarDec = self._sun_declination(t)

        try:
            hourangle = self._hour_angle_sunrise(latitude, solarDec)
        except:
            raise AstralError(('Sun remains below horizon '
                'on this day, at this location.'))

        delta = -longitude - degrees(hourangle)
        timeDiff = 4.0 * delta
        timeUTC = 720.0 + timeDiff - eqtime

        newt = self._jday_to_jcentury(self._jcentury_to_jday(t) + \
            timeUTC / 1440.0)
        eqtime = self._eq_of_time(newt)
        solarDec = self._sun_declination(newt)
        hourangle = self._hour_angle_dawn(latitude, solarDec, self._depression)
        delta = -longitude - degrees(hourangle)
        timeDiff = 4 * delta
        timeUTC = 720 + timeDiff - eqtime

        timeUTC = timeUTC/60.0
        hour = int(timeUTC)
        minute = int((timeUTC - hour) * 60)
        second = int((((timeUTC - hour) * 60) - minute) * 60)

        if second > 59:
            second -= 60
            minute += 1
        elif second < 0:
            second += 60
            minute -= 1

        if minute > 59:
            minute -= 60
            hour += 1
        elif minute < 0:
            minute += 60
            hour -= 1

        if hour > 23:
            hour -= 24
            date += datetime.timedelta(days=1)
        elif hour < 0:
            hour += 24
            date -= datetime.timedelta(days=1)

        dawn = datetime.datetime(date.year, date.month, date.day, hour, minute,
            second, tzinfo=pytz.utc)

        return dawn

    def sunrise_utc(self, date, latitude, longitude):
        """Calculate sunrise time in the UTC timezone.

        :param date:       Date to calculate for.
        :type date:        datetime.date
        :param latitude:   Latitude - Southern latitudes should be negative
        :type latitude:    float
        :param longitude:  Longitude as a floating point number. Eastern
                           latitudes should be negative
        :type longitude:   float

        :rtype: date/time in UTC timezone
        """

        julianday = self._julianday(date)

        t = self._jday_to_jcentury(julianday)
        eqtime = self._eq_of_time(t)
        solarDec = self._sun_declination(t)

        try:
            hourangle = self._hour_angle_sunrise(latitude, solarDec)
        except:
            raise AstralError(('Sun remains below horizon on this day, '
                'at this location.'))

        delta = -longitude - degrees(hourangle)
        timeDiff = 4.0 * delta
        timeUTC = 720.0 + timeDiff - eqtime

        newt = self._jday_to_jcentury(self._jcentury_to_jday(t) + \
            timeUTC / 1440.0)
        eqtime = self._eq_of_time(newt)
        solarDec = self._sun_declination(newt)
        hourangle = self._hour_angle_sunrise(latitude, solarDec)
        delta = -longitude - degrees(hourangle)
        timeDiff = 4 * delta
        timeUTC = 720 + timeDiff - eqtime

        timeUTC = timeUTC/60.0
        hour = int(timeUTC)
        minute = int((timeUTC - hour) * 60)
        second = int((((timeUTC - hour) * 60) - minute) * 60)

        if second > 59:
            second -= 60
            minute += 1
        elif second < 0:
            second += 60
            minute -= 1

        if minute > 59:
            minute -= 60
            hour += 1
        elif minute < 0:
            minute += 60
            hour -= 1

        if hour > 23:
            hour -= 24
            date += datetime.timedelta(days=1)
        elif hour < 0:
            hour += 24
            date -= datetime.timedelta(days=1)

        sunrise = datetime.datetime(date.year, date.month, date.day,
            hour, minute, second, tzinfo=pytz.utc)

        return sunrise

    def solar_noon_utc(self, date, longitude):
        """Calculate solar noon time in the UTC timezone.

        :param date:       Date to calculate for.
        :type date:        datetime.date
        :param latitude:   Latitude - Southern latitudes should be negative
        :type latitude:    float
        :param longitude:  Longitude as a floating point number. Eastern
                           latitudes should be negative
        :type longitude:   float

        :rtype: date/time in UTC timezone
        """

        julianday = self._julianday(date)

        newt = self._jday_to_jcentury(julianday + 0.5 + -longitude / 360.0)

        eqtime = self._eq_of_time(newt)
        timeUTC = 720.0 + (-longitude * 4.0) - eqtime

        timeUTC = timeUTC/60.0
        hour = int(timeUTC)
        minute = int((timeUTC - hour) * 60)
        second = int((((timeUTC - hour) * 60) - minute) * 60)

        if second > 59:
            second -= 60
            minute += 1
        elif second < 0:
            second += 60
            minute -= 1

        if minute > 59:
            minute -= 60
            hour += 1
        elif minute < 0:
            minute += 60
            hour -= 1

        if hour > 23:
            hour -= 24
            date += datetime.timedelta(days=1)
        elif hour < 0:
            hour += 24
            date -= datetime.timedelta(days=1)

        noon = datetime.datetime(date.year, date.month, date.day,
            hour, minute, second, tzinfo=pytz.utc)

        return noon

    def sunset_utc(self, date, latitude, longitude):
        """Calculate sunset time in the UTC timezone.

        :param date:       Date to calculate for.
        :type date:        datetime.date
        :param latitude:   Latitude - Southern latitudes should be negative
        :type latitude:    float
        :param longitude:  Longitude as a floating point number. Eastern
                           latitudes should be negative
        :type longitude:   float

        :rtype: date/time in UTC timezone
        """

        julianday = self._julianday(date)

        t = self._jday_to_jcentury(julianday)
        eqtime = self._eq_of_time(t)
        solarDec = self._sun_declination(t)

        try:
            hourangle = self._hour_angle_sunset(latitude, solarDec)
        except:
            raise AstralError(('Sun remains below horizon on this day, '
                'at this location.'))

        delta = -longitude - degrees(hourangle)
        timeDiff = 4.0 * delta
        timeUTC = 720.0 + timeDiff - eqtime

        newt = self._jday_to_jcentury(self._jcentury_to_jday(t) + \
            timeUTC / 1440.0)
        eqtime = self._eq_of_time(newt)
        solarDec = self._sun_declination(newt)
        hourangle = self._hour_angle_sunset(latitude, solarDec)
        delta = -longitude - degrees(hourangle)
        timeDiff = 4 * delta
        timeUTC = 720 + timeDiff - eqtime

        timeUTC = timeUTC/60.0
        hour = int(timeUTC)
        minute = int((timeUTC - hour) * 60)
        second = int((((timeUTC - hour) * 60) - minute) * 60)

        if second > 59:
            second -= 60
            minute += 1
        elif second < 0:
            second += 60
            minute -= 1

        if minute > 59:
            minute -= 60
            hour += 1
        elif minute < 0:
            minute += 60
            hour -= 1

        if hour > 23:
            hour -= 24
            date += datetime.timedelta(days=1)
        elif hour < 0:
            hour += 24
            date -= datetime.timedelta(days=1)

        sunset = datetime.datetime(date.year, date.month, date.day,
            hour, minute, second, tzinfo=pytz.utc)

        return sunset

    def dusk_utc(self, date, latitude, longitude):
        """Calculate dusk time in the UTC timezone.

        :param date:       Date to calculate for.
        :type date:        datetime.date
        :param latitude:   Latitude - Southern latitudes should be negative
        :type latitude:    float
        :param longitude:  Longitude as a floating point number. Eastern
                           latitudes should be negative
        :type longitude:   float

        :rtype: date/time in UTC timezone
        """

        julianday = self._julianday(date)

        if latitude > 89.8:
            latitude = 89.8

        if latitude < -89.8:
            latitude = -89.8

        t = self._jday_to_jcentury(julianday)
        eqtime = self._eq_of_time(t)
        solarDec = self._sun_declination(t)

        try:
            hourangle = self._hour_angle_sunset(latitude, solarDec)
        except:
            raise AstralError(('Sun remains below horizon on this day, '
                'at this location.'))

        delta = -longitude - degrees(hourangle)
        timeDiff = 4.0 * delta
        timeUTC = 720.0 + timeDiff - eqtime

        newt = self._jday_to_jcentury(self._jcentury_to_jday(t) + \
            timeUTC / 1440.0)
        eqtime = self._eq_of_time(newt)
        solarDec = self._sun_declination(newt)
        hourangle = self._hour_angle_dusk(latitude, solarDec, self._depression)
        delta = -longitude - degrees(hourangle)
        timeDiff = 4 * delta
        timeUTC = 720 + timeDiff - eqtime

        timeUTC = timeUTC/60.0
        hour = int(timeUTC)
        minute = int((timeUTC - hour) * 60)
        second = int((((timeUTC - hour) * 60) - minute) * 60)

        if second > 59:
            second -= 60
            minute += 1
        elif second < 0:
            second += 60
            minute -= 1

        if minute > 59:
            minute -= 60
            hour += 1
        elif minute < 0:
            minute += 60
            hour -= 1

        if hour > 23:
            hour -= 24
            date += datetime.timedelta(days=1)
        elif hour < 0:
            hour += 24
            date -= datetime.timedelta(days=1)

        dusk = datetime.datetime(date.year, date.month, date.day,
            hour, minute, second, tzinfo=pytz.utc)

        return dusk

    def rahukaalam_utc(self, date, latitude, longitude):
        """Calculate ruhakaalam times in the UTC timezone.

        :param date:       Date to calculate for.
        :type date:        datetime.date
        :param latitude:   Latitude - Southern latitudes should be negative
        :type latitude:    float
        :param longitude:  Longitude as a floating point number. Eastern
                           latitudes should be negative
        :type longitude:   float

        :rtype: Dictionary with keys ``start`` and ``end``
        """

        if date is None:
            date = datetime.date.today()

        try:
            sunrise = self.sunrise_utc(date, latitude, longitude)
            sunset = self.sunset_utc(date, latitude, longitude)
        except:
            raise AstralError(('Sun remains below horizon on this day, '
                'at this location.'))

        octant_duration = (sunset - sunrise) / 8

        # Mo,Sa,Fr,We,Th,Tu,Su
        octant_index = [1,6,4,5,3,2,7]

        weekday = date.weekday()
        octant = octant_index[weekday]

        start = sunrise + (octant_duration * octant)
        end = start + octant_duration

        return {'start': start, 'end': end}

    def solar_azimuth(self, dateandtime, latitude, longitude):
        """Calculate the azimuth of the sun in the UTC timezone.

        :param dateandtime:       Date/time to calculate for.
        :type dateandtime:        datetime.datetime
        :param latitude:   Latitude - Southern latitudes should be negative
        :type latitude:    float
        :param longitude:  Longitude as a floating point number. Eastern
                           latitudes should be negative
        :type longitude:   float

        :rtype: Azimuth in degrees
        """

        if latitude > 89.8:
            latitude = 89.8

        if latitude < -89.8:
            latitude = -89.8

        zone = -dateandtime.utcoffset().seconds / 3600.0
        utc_datetime = dateandtime.astimezone(pytz.utc)
        timenow = utc_datetime.hour + (utc_datetime.minute / 60.0) + \
            (utc_datetime.second // 3600)

        JD = self._julianday(dateandtime)
        t = self._jday_to_jcentury(JD + timenow / 24.0)
        theta = self._sun_declination(t)
        Etime = self._eq_of_time(t)

        eqtime = Etime
        solarDec = theta   # in degrees

        solarTimeFix = eqtime - (4.0 * -longitude) + (60 * zone)
        trueSolarTime = dateandtime.hour * 60.0 + dateandtime.minute + \
            dateandtime.second / 60.0 + solarTimeFix
        #    in minutes

        while trueSolarTime > 1440:
            trueSolarTime = trueSolarTime - 1440

        hourangle = trueSolarTime / 4.0 - 180.0
        #    Thanks to Louis Schwarzmayr for the next line:
        if hourangle < -180:
            hourangle = hourangle + 360.0

        harad = radians(hourangle)

        csz = sin(radians(latitude)) * sin(radians(solarDec)) + \
              cos(radians(latitude)) * cos(radians(solarDec)) * cos(harad)

        if csz > 1.0:
            csz = 1.0
        elif csz < -1.0:
            csz = -1.0

        zenith = degrees(acos(csz))

        azDenom = (cos(radians(latitude)) * sin(radians(zenith)))

        if (abs(azDenom) > 0.001):
            azRad = ((sin(radians(latitude)) *  cos(radians(zenith))) - \
                sin(radians(solarDec))) / azDenom

            if abs(azRad) > 1.0:
                if azRad < 0:
                    azRad = -1.0
                else:
                    azRad = 1.0

            azimuth = 180.0 - degrees(acos(azRad))

            if hourangle > 0.0:
                azimuth = -azimuth
        else:
            if latitude > 0.0:
                azimuth = 180.0
            else:
                azimuth = 0#

        if azimuth < 0.0:
            azimuth = azimuth + 360.0

        return azimuth

    def solar_elevation(self, dateandtime, latitude, longitude):
        """Calculate the elevation of the sun.

        :param dateandtime:       Date/time to calculate for.
        :type dateandtime:        datetime.datetime
        :param latitude:   Latitude - Southern latitudes should be negative
        :type latitude:    float
        :param longitude:  Longitude as a floating point number. Eastern
                           latitudes should be negative
        :type longitude:   float

        :rtype: Elevation in degrees
        """

        if latitude > 89.8:
            latitude = 89.8

        if latitude < -89.8:
            latitude = -89.8

        zone = -dateandtime.utcoffset().seconds / 3600.0
        utc_datetime = dateandtime.astimezone(pytz.utc)
        timenow = utc_datetime.hour + (utc_datetime.minute / 60.0) + \
            (utc_datetime.second // 3600)

        JD = self._julianday(dateandtime)
        t = self._jday_to_jcentury(JD + timenow / 24.0)
        theta = self._sun_declination(t)
        Etime = self._eq_of_time(t)

        eqtime = Etime
        solarDec = theta   # in degrees

        solarTimeFix = eqtime - (4.0 * -longitude) + (60 * zone)
        trueSolarTime = dateandtime.hour * 60.0 + dateandtime.minute + \
            dateandtime.second / 60.0 + solarTimeFix
        #    in minutes

        while trueSolarTime > 1440:
            trueSolarTime = trueSolarTime - 1440

        hourangle = trueSolarTime / 4.0 - 180.0
        #    Thanks to Louis Schwarzmayr for the next line:
        if hourangle < -180:
            hourangle = hourangle + 360.0

        harad = radians(hourangle)

        csz = sin(radians(latitude)) * sin(radians(solarDec)) + \
              cos(radians(latitude)) * cos(radians(solarDec)) * cos(harad)

        if csz > 1.0:
            csz = 1.0
        elif csz < -1.0:
            csz = -1.0

        zenith = degrees(acos(csz))

        azDenom = (cos(radians(latitude)) * sin(radians(zenith)))

        if (abs(azDenom) > 0.001):
            azRad = ((sin(radians(latitude)) *  cos(radians(zenith))) - \
                sin(radians(solarDec))) / azDenom

            if abs(azRad) > 1.0:
                if azRad < 0:
                    azRad = -1.0
                else:
                    azRad = 1.0

            azimuth = 180.0 - degrees(acos(azRad))

            if hourangle > 0.0:
                azimuth = -azimuth
        else:
            if latitude > 0.0:
                azimuth = 180.0
            else:
                azimuth = 0

        if azimuth < 0.0:
            azimuth = azimuth + 360.0

        exoatmElevation = 90.0 - zenith

        if exoatmElevation > 85.0:
            refractionCorrection = 0.0
        else:
            te = tan(radians(exoatmElevation))
            if exoatmElevation > 5.0:
                refractionCorrection = 58.1 / te - 0.07 / (te * te * te) + \
                    0.000086 / (te * te * te * te * te)
            elif exoatmElevation > -0.575:
                step1 = (-12.79 + exoatmElevation * 0.711)
                step2 = (103.4 + exoatmElevation * (step1))
                step3 = (-518.2 + exoatmElevation * (step2))
                refractionCorrection = 1735.0 + exoatmElevation * (step3)
            else:
                refractionCorrection = -20.774 / te

            refractionCorrection = refractionCorrection / 3600.0

        solarzen = zenith - refractionCorrection

        solarelevation = 90.0 - solarzen

        return solarelevation

    def moon_phase(self, date, tz):
        """Calculates the phase of the moon on the specified date.

        :param date: The date to calculate the phase for.
        :type date: datetime.date
        :param tz: The timezone to calculate the phase for.
        :type tz: pytz.tz

        :rtype:
            Integer designating phase

                | 0  = New moon
                | 7  = First quarter
                | 14 = Full moon
                | 21 = Last quarter
        """

        jd = self._julianday(date, tz)
        DT = pow((jd - 2382148), 2) / (41048480*86400)
        T = (jd + DT - 2451545.0) / 36525
        T2 = pow(T,2)
        T3 = pow(T,3)
        D = 297.85 + (445267.1115*T) - (0.0016300*T2) + (T3/545868)
        D = radians(self._proper_angle(D))
        M = 357.53 + (35999.0503*T)
        M = radians(self._proper_angle(M))
        M1 = 134.96 + (477198.8676*T) + (0.0089970*T2) + (T3/69699)
        M1 = radians(self._proper_angle(M1))
        elong = degrees(D) + 6.29*sin(M1)
        elong -= 2.10*sin(M)
        elong += 1.27*sin(2*D - M1)
        elong += 0.66*sin(2*D)
        elong = self._proper_angle(elong)
        moon = int(floor(((elong + 6.43) / 360) * 28))
        if moon == 28:
            moon = 0

        return moon

    def _proper_angle(self, value):
        if value > 0.0:
            value /= 360.0
            return (value - floor(value)) * 360
        else:
            tmp = ceil(abs(value / 360.0))
            return value + tmp * 360.0

    def _julianday(self, date, timezone=None):
        day = date.day
        month = date.month
        year = date.year

        if timezone is not None:
            offset = timezone.localize(datetime.datetime(year, month, day)).utcoffset()
            offset = offset.total_seconds() / 1440.0
            day += offset + 0.5

        if month <= 2:
            year = year - 1
            month = month + 12

        A = floor(year / 100.0)
        B = 2 - A + floor(A / 4.0)

        jd = floor(365.25 * (year + 4716)) + floor(30.6001 * (month + 1)) + \
            day - 1524.5
        if jd > 2299160.4999999:
            jd += B

        return jd

    def _jday_to_jcentury(self, julianday):
        return (julianday - 2451545.0) / 36525.0

    def _jcentury_to_jday(self, juliancentury):
        return (juliancentury * 36525.0) + 2451545.0

    def _mean_obliquity_of_ecliptic(self, juliancentury):
        seconds = 21.448 - juliancentury * \
            (46.815 + juliancentury * (0.00059 - juliancentury * (0.001813)))
        return 23.0 + (26.0 + (seconds / 60.0)) / 60.0

    def _obliquity_correction(self, juliancentury):
        e0 = self._mean_obliquity_of_ecliptic(juliancentury)

        omega = 125.04 - 1934.136 * juliancentury
        return e0 + 0.00256 * cos(radians(omega))

    def _geom_mean_long_sun(self, juliancentury):
        l0 = 280.46646 + \
            juliancentury * (36000.76983 + 0.0003032 * juliancentury)
        return l0 % 360.0

    def _eccentricity_earth_orbit(self, juliancentury):
        return 0.016708634 - \
            juliancentury * (0.000042037 + 0.0000001267 * juliancentury)

    def _geom_mean_anomaly_sun(self, juliancentury):
        return 357.52911 + \
            juliancentury * (35999.05029 - 0.0001537 * juliancentury)

    def _eq_of_time(self, juliancentury):
        epsilon = self._obliquity_correction(juliancentury)
        l0 = self._geom_mean_long_sun(juliancentury)
        e = self._eccentricity_earth_orbit(juliancentury)
        m = self._geom_mean_anomaly_sun(juliancentury)

        y = tan(radians(epsilon) / 2.0)
        y = y * y

        sin2l0 = sin(2.0 * radians(l0))
        sinm = sin(radians(m))
        cos2l0 = cos(2.0 * radians(l0))
        sin4l0 = sin(4.0 * radians(l0))
        sin2m = sin(2.0 * radians(m))

        Etime = y * sin2l0 - 2.0 * e * sinm + 4.0 * e * y * sinm * cos2l0 - \
                0.5 * y * y * sin4l0 - 1.25 * e * e * sin2m

        return degrees(Etime) * 4.0

    def _sun_eq_of_center(self, juliancentury):
        m = self._geom_mean_anomaly_sun(juliancentury)

        mrad = radians(m)
        sinm = sin(mrad)
        sin2m = sin(mrad + mrad)
        sin3m = sin(mrad + mrad + mrad)

        c = sinm * (1.914602 - juliancentury * \
            (0.004817 + 0.000014 * juliancentury)) + \
            sin2m * (0.019993 - 0.000101 * juliancentury) + sin3m * 0.000289

        return c

    def _sun_true_long(self, juliancentury):
        l0 = self._geom_mean_long_sun(juliancentury)
        c = self._sun_eq_of_center(juliancentury)

        return l0 + c

    def _sun_apparent_long(self, juliancentury):
        O = self._sun_true_long(juliancentury)

        omega = 125.04 - 1934.136 * juliancentury
        return O - 0.00569 - 0.00478 * sin(radians(omega))

    def _sun_declination(self, juliancentury):
        e = self._obliquity_correction(juliancentury)
        lambd = self._sun_apparent_long(juliancentury)

        sint = sin(radians(e)) * sin(radians(lambd))
        return degrees(asin(sint))

    def _hour_angle(self, latitude, solar_dec, solar_depression):
        latRad = radians(latitude)
        sdRad = radians(solar_dec)

        HA = (acos(cos(radians(90 + solar_depression)) / \
            (cos(latRad) * cos(sdRad)) - tan(latRad) * tan(sdRad)))

        return HA

    def _hour_angle_sunrise(self, latitude, solar_dec):
        return self._hour_angle(latitude, solar_dec, 0.833)

    def _hour_angle_sunset(self, latitude, solar_dec):
        return -self._hour_angle(latitude, solar_dec, 0.833)

    def _hour_angle_dawn(self, latitude, solar_dec, solar_depression):
        return self._hour_angle(latitude, solar_dec, solar_depression)

    def _hour_angle_dusk(self, latitude, solar_dec, solar_depression):
        return -self._hour_angle(latitude, solar_dec, solar_depression)

    def _sun_true_anomoly(self, juliancentury):
        m = self._geom_mean_anomaly_sun(juliancentury)
        c = self._sun_eq_of_center(juliancentury)

        return m + c

    def _sun_rad_vector(self, juliancentury):
        v = self._sun_true_anomoly(juliancentury)
        e = self._eccentricity_earth_orbit(juliancentury)

        return (1.000001018 * (1 - e * e)) / (1 + e * cos(radians(v)))

    def _sun_rt_ascension(self, juliancentury):
        e = self._obliquity_correction(juliancentury)
        lambd = self._sun_apparent_long(juliancentury)

        tananum = (cos(radians(e)) * sin(radians(lambd)))
        tanadenom = (cos(radians(lambd)))

        return degrees(atan2(tananum, tanadenom))


# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""Multiprocessing and multithreading setup"""
import importlib
import logging
from enum import Enum
from gammapy.utils.pbar import progress_bar

log = logging.getLogger(__name__)

class ParallelBackendEnum(Enum):
    """Enum for parallel backend"""

    multiprocessing = "multiprocessing"
    ray = "ray"

    @classmethod
    def from_str(cls, value):
        """Get enum from string"""
        if value is None:
            value = BACKEND_DEFAULT

        if value == "ray" and not is_ray_available():
            log.warning("Ray is not installed, falling back to multiprocessing backend")
            value = "multiprocessing"

        return cls(value)

class PoolMethodEnum(Enum):
    """Enum for pool method"""

    starmap = "starmap"
    apply_async = "apply_async"

BACKEND_DEFAULT = ParallelBackendEnum.multiprocessing
N_JOBS_DEFAULT = 1

def get_multiprocessing():
    """Get multiprocessing module"""
    import multiprocessing

    return multiprocessing

def get_multiprocessing_ray():
    """Get multiprocessing module for ray backend"""
    import ray.util.multiprocessing as multiprocessing

    log.warning(
        "Gammapy support for parallelisation with ray is still a prototype and is not fully functional."
    )
    return multiprocessing

def is_ray_initialized():
    """Check if ray is initialized"""
    try:
        from ray import is_initialized

        return is_initialized()
    except ModuleNotFoundError:
        return False

def is_ray_available():
    """Check if ray is available"""
    try:
        importlib.import_module("ray")
        return True
    except ModuleNotFoundError:
        return False

class ParallelMixin:
    """Mixin class to handle parallel processing"""

    @property
    def n_jobs(self):
        """Number of jobs (int)"""
        # TODO: this is somewhat unusual behaviour. It deviates from a normal default value handling
        if self._n_jobs is None:
            return N_JOBS_DEFAULT

        return self._n_jobs

    @n_jobs.setter
    def n_jobs(self, value):
        """Number of jobs setter (int)"""
        if not isinstance(value, (int, type(None))):
            raise ValueError(
                f"Invalid type: {value!r}, and integer or None is expected."
            )

        self._n_jobs = value

    @property
    def parallel_backend(self):
        """Parallel backend (str)"""
        if self._parallel_backend is None:
            return BACKEND_DEFAULT

        return self._parallel_backend

    @parallel_backend.setter
    def parallel_backend(self, value):
        """Parallel backend setter (str)"""
        self._parallel_backend = ParallelBackendEnum.from_str(value).value

def run_multiprocessing(
    func,
    inputs,
    backend=None,
    pool_kwargs=None,
    method="starmap",
    method_kwargs=None,
    task_name="",
):
    """Run function in a loop or in Parallel

    Notes
    -----
    The progress bar can be displayed for this function.

    Parameters
    ----------
    func : function
        Function to run
    inputs : list
        List of arguments to pass to the function
    backend : {'multiprocessing', 'ray'}
        Backend to use.
    pool_kwargs : dict
        Keyword arguments passed to the pool. The number of processes is limited
        to the number of physical CPUs.
    method : {'starmap', 'apply_async'}
        Pool method to use.
    method_kwargs : dict
        Keyword arguments passed to the method
    task_name : str
        Name of the task to display in the progress bar
    """
    backend = ParallelBackendEnum.from_str(backend)

    if method_kwargs is None:
        method_kwargs = {}

    if pool_kwargs is None:
        pool_kwargs = {}

    processes = pool_kwargs.get("processes", N_JOBS_DEFAULT)

    multiprocessing = PARALLEL_BACKEND_MODULES[backend]()

    if backend == ParallelBackendEnum.multiprocessing:
        cpu_count = multiprocessing.cpu_count()

        if processes > cpu_count:
            log.info(f"Limiting number of processes from {processes} to {cpu_count}")
            processes = cpu_count

        if multiprocessing.current_process().name != "MainProcess":
            # subprocesses cannot have childs
            processes = 1
    # TODO: check for ray

    if processes == 1:
        return run_loop(
            func=func, inputs=inputs, method_kwargs=method_kwargs, task_name=task_name
        )

    if backend == ParallelBackendEnum.ray:
        address = "auto" if is_ray_initialized() else None
        pool_kwargs.setdefault("ray_address", address)

    log.info(f"Using {processes} processes to compute {task_name}")

    with multiprocessing.Pool(**pool_kwargs) as pool:
        pool_func = POOL_METHODS[PoolMethodEnum(method)]
        results = pool_func(
            pool=pool,
            func=func,
            inputs=inputs,
            method_kwargs=method_kwargs,
            task_name=task_name,
        )

    return results

def run_loop(func, inputs, method_kwargs=None, task_name=""):
    """Loop over inputs an run function"""
    results = []

    callback = method_kwargs.get("callback", None)

    for arguments in progress_bar(inputs, desc=task_name):
        result = func(*arguments)

        if callback is not None:
            result = callback(result)

        results.append(result)

    return results

def run_pool_star_map(pool, func, inputs, method_kwargs=None, task_name=""):
    """Run function in parallel"""
    return pool.starmap(func, progress_bar(inputs, desc=task_name), **method_kwargs)

def run_pool_async(pool, func, inputs, method_kwargs=None, task_name=""):
    """Run function in parallel async"""
    results = []

    for arguments in progress_bar(inputs, desc=task_name):
        result = pool.apply_async(func, arguments, **method_kwargs)
        results.append(result)
    # wait async run is done
    [result.wait() for result in results]
    return results

POOL_METHODS = {
    PoolMethodEnum.starmap: run_pool_star_map,
    PoolMethodEnum.apply_async: run_pool_async,
}

PARALLEL_BACKEND_MODULES = {
    ParallelBackendEnum.multiprocessing: get_multiprocessing,
    ParallelBackendEnum.ray: get_multiprocessing_ray,
}


'''
Created on Nov 4, 2010
Chapter 5 source file for Machine Learing in Action
@author: Peter
'''
from numpy import *
from time import sleep
import matplotlib.pyplot as plt
import numpy as np

#SMO算法相关辅助中的辅助函数
# 1 解析文本数据函数，
# 提取每个样本的特征组成向量，添加到数据矩阵
# 添加样本标签到标签向量
def loadDataSet(fileName):               #文件路径
    dataMat = []; labelMat = []  #数据列表和标签列表
    fr = open(fileName)          #打开文件
    for line in fr.readlines():  #逐行读取
        lineArr = line.strip().split('\t')  #去除首尾空格，并拆分字符串
        dataMat.append([float(lineArr[0]), float(lineArr[1])]) #将第一二列加入数据列表
        labelMat.append(float(lineArr[2])) #将第三列加入标签列表
    return dataMat,labelMat

# 函数说明:数据可视化
# dataMat - 数据矩阵
# labelMat - 数据标签
# Returns:无
def showDataSet(dataMat, labelMat):
    data_plus = []                                  #正样本
    data_minus = []                                 #负样本
    for i in range(len(dataMat)):
        if labelMat[i] > 0:
            data_plus.append(dataMat[i])
        else:
            data_minus.append(dataMat[i])
    data_plus_np = np.array(data_plus)              #转换为numpy矩阵
    data_minus_np = np.array(data_minus)            #转换为numpy矩阵
    plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1])   #正样本散点图
    plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1]) #负样本散点图
    plt.show()

#2 在样本集中采用随机选择的方法选取第二个不等于第一个alphai的
#优化向量alphaj
def selectJrand(i,m):
    j=i
    while (j==i):
        j = int(random.uniform(0,m))
    return j

#3 约束范围L<=alphaj<=H内的更新后的alphaj值
def clipAlpha(aj,H,L):
    if aj > H:
        aj = H
    if L > aj:
        aj = L
    return aj

# #测试
# dataMat, labelMat=loadDataSet('testSet.txt')
# showDataSet(dataMat, labelMat)

#dataMat    ：数据列表
#classLabels：标签列表
#C          ：权衡因子（增加松弛因子而在目标优化函数中引入了惩罚项）
#toler      ：容错率
#maxIter    ：最大迭代次数
def smoSimple(dataMatIn, classLabels, C, toler, maxIter):
    dataMatrix = mat(dataMatIn); labelMat = mat(classLabels).transpose() # 将列表形式转为矩阵或向量形式
    b = 0; m,n = shape(dataMatrix)   # 初始化b=0，获取矩阵行列
    alphas = mat(zeros((m,1)))       # 初始化alpha参数，设为0
    iter = 0                         #迭代次数为0
    while (iter < maxIter):          #当迭代次数小于最大迭代次数时(w外循环)
        alphaPairsChanged = 0        #改变的alpha对数
        for i in range(m):           #遍历样本集中样本
            # 步骤1：计算误差Ei
            fXi = float(multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[i,:].T)) + b #计算支持向量机算法的预测值
            Ei = fXi - float(labelMat[i])#计算预测值与实际值的误差
            #优化alpha，更设定一定的容错率。
            if ((labelMat[i]*Ei < -toler) and (alphas[i] < C)) or ((labelMat[i]*Ei > toler) and (alphas[i] > 0)):
                j = selectJrand(i,m) #随机选择另一个与alpha_i成对优化的alpha_j
                # 步骤1：计算误差Ej
                fXj = float(multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[j,:].T)) + b #计算第二个变量对应数据的预测值
                Ej = fXj - float(labelMat[j]) #计算与测试与实际值的差值
                alphaIold = alphas[i].copy(); alphaJold = alphas[j].copy(); #记录alphai和alphaj的原始值，便于后续的比较
                # 步骤2：计算上下界L和H
                if (labelMat[i] != labelMat[j]):  #如果两个alpha对应样本的标签不相同
                    L = max(0, alphas[j] - alphas[i])  #求出相应的上下边界
                    H = min(C, C + alphas[j] - alphas[i])
                else:
                    L = max(0, alphas[j] + alphas[i] - C)
                    H = min(C, alphas[j] + alphas[i])
                if L==H: print ("L==H"); continue
                # 根据公式计算未经剪辑的alphaj
                # ------------------------------------------
                eta = 2.0 * dataMatrix[i,:]*dataMatrix[j,:].T - dataMatrix[i,:]*dataMatrix[i,:].T - dataMatrix[j,:]*dataMatrix[j,:].T
                # 如果eta>=0,跳出本次循环
                if eta >= 0: print ("eta>=0"); continue
                # 步骤4：更新alpha_j
                alphas[j] -= labelMat[j]*(Ei - Ej)/eta
                # 步骤5：修剪alpha_j
                alphas[j] = clipAlpha(alphas[j],H,L)
                # ------------------------------------------
                # 如果改变后的alphaj值变化不大，跳出本次循环
                if (abs(alphas[j] - alphaJold) < 0.00001): print ("alpha_j变化太小"); continue
                # 步骤6：更新alpha_i
                alphas[i] += labelMat[j]*labelMat[i]*(alphaJold - alphas[j])
                # 步骤7：更新b_1和b_2
                b1 = b - Ei- labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i,:]*dataMatrix[i,:].T - labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[i,:]*dataMatrix[j,:].T
                b2 = b - Ej- labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i,:]*dataMatrix[j,:].T - labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[j,:]*dataMatrix[j,:].T
                # 步骤8：根据b_1和b_2更新b
                if (0 < alphas[i]) and (C > alphas[i]): b = b1
                # 否则如果0<alphai<C,那么b=b1
                elif (0 < alphas[j]) and (C > alphas[j]): b = b2
                # 否则，alphai，alphaj=0或C
                else: b = (b1 + b2)/2.0
                # 统计优化次数
                alphaPairsChanged += 1
                print("第%d次迭代 样本:%d, alpha优化次数:%d" % (iter, i, alphaPairsChanged))
        # 最后判断是否有改变的alpha对，没有就进行下一次迭代
        if (alphaPairsChanged == 0): iter += 1
        # 否则，迭代次数置0，继续循环
        else: iter = 0
        print("迭代次数: %d" % iter)
    # 返回最后的b值和alpha向量
    return b,alphas

# 函数说明:分类结果可视化
# dataMat - 数据矩阵
# w - 直线法向量
# b - 直线解决
# Returns:无
def showClassifer(dataMat, w, b):
    #绘制样本点
    data_plus = []                                  #正样本
    data_minus = []                                 #负样本
    for i in range(len(dataMat)):
        if labelMat[i] > 0:
            data_plus.append(dataMat[i])
        else:
            data_minus.append(dataMat[i])
    data_plus_np = np.array(data_plus)              #转换为numpy矩阵
    data_minus_np = np.array(data_minus)            #转换为numpy矩阵
    plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1], s=30, alpha=0.7)   #正样本散点图
    plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1], s=30, alpha=0.7) #负样本散点图
    #绘制直线
    x1 = max(dataMat)[0]
    x2 = min(dataMat)[0]
    a1, a2 = w
    b = float(b)
    a1 = float(a1[0])
    a2 = float(a2[0])
    y1, y2 = (-b- a1*x1)/a2, (-b - a1*x2)/a2
    plt.plot([x1, x2], [y1, y2])
    #找出支持向量点
    for i, alpha in enumerate(alphas):
        if abs(alpha) > 0:
            x, y = dataMat[i]
            plt.scatter([x], [y], s=150, c='none', alpha=0.7, linewidth=1.5, edgecolor='red')
    plt.show()

# 函数说明:计算w
# dataMat - 数据矩阵
# labelMat - 数据标签
# alphas - alphas值
# Returns:无
def get_w(dataMat, labelMat, alphas):
    alphas, dataMat, labelMat = np.array(alphas), np.array(dataMat), np.array(labelMat)
    w = np.dot((np.tile(labelMat.reshape(1, -1).T, (1, 2)) * dataMat).T, alphas)
    return w.tolist()

# #测试
# dataMat, labelMat = loadDataSet('testSet.txt')
# b,alphas = smoSimple(dataMat, labelMat, 0.6, 0.001, 40)
# w = get_w(dataMat, labelMat, alphas)
# showClassifer(dataMat, w, b)

def kernelTrans(X, A, kTup): #calc the kernel or transform data to a higher dimensional space
    m,n = shape(X)
    K = mat(zeros((m,1)))
    if kTup[0]=='lin': K = X * A.T   #linear kernel
    elif kTup[0]=='rbf':
        for j in range(m):
            deltaRow = X[j,:] - A
            K[j] = deltaRow*deltaRow.T
        K = exp(K/(-1*kTup[1]**2)) #divide in NumPy is element-wise not matrix like Matlab
    else: raise NameError('Houston We Have a Problem -- \
    That Kernel is not recognized')
    return K

#启发式SMO算法的支持函数
#新建一个类的数据结构，保存当前重要的值
class optStruct:
    def __init__(self,dataMatIn, classLabels, C, toler, kTup):  # Initialize the structure with the parameters
        self.X = dataMatIn
        self.labelMat = classLabels
        self.C = C
        self.tol = toler
        self.m = shape(dataMatIn)[0]
        self.alphas = mat(zeros((self.m,1)))
        self.b = 0
        self.eCache = mat(zeros((self.m,2))) #first column is valid flag
        self.K = mat(zeros((self.m,self.m)))
        for i in range(self.m):
            self.K[:,i] = kernelTrans(self.X, self.X[i,:], kTup)

#格式化计算误差的函数，方便多次调用
def calcEk(oS, k):
    fXk = float(multiply(oS.alphas,oS.labelMat).T*oS.K[:,k] + oS.b)
    Ek = fXk - float(oS.labelMat[k])
    return Ek

#修改选择第二个变量alphaj的方法
def selectJ(i, oS, Ei):
    maxK = -1; maxDeltaE = 0; Ej = 0
    oS.eCache[i] = [1,Ei]
    validEcacheList = nonzero(oS.eCache[:,0].A)[0] #将误差矩阵每一行第一列置1，以此确定出误差不为0的样本
    if (len(validEcacheList)) > 1:
        for k in validEcacheList:   #loop through valid Ecache values and find the one that maximizes delta E
            if k == i: continue #don't calc for i, waste of time
            Ek = calcEk(oS, k)
            deltaE = abs(Ei - Ek)
            if (deltaE > maxDeltaE):
                maxK = k; maxDeltaE = deltaE; Ej = Ek
        return maxK, Ej
    else:   #否则，就从样本集中随机选取alphaj
        j = selectJrand(i, oS.m)
        Ej = calcEk(oS, j)
    return j, Ej

#更新误差矩阵
def updateEk(oS, k):#after any alpha has changed update the new value in the cache
    Ek = calcEk(oS, k)
    oS.eCache[k] = [1,Ek]

def innerL(i, oS):
    Ei = calcEk(oS, i)
    if ((oS.labelMat[i]*Ei < -oS.tol) and (oS.alphas[i] < oS.C)) or ((oS.labelMat[i]*Ei > oS.tol) and (oS.alphas[i] > 0)):
        j,Ej = selectJ(i, oS, Ei) #this has been changed from selectJrand
        alphaIold = oS.alphas[i].copy(); alphaJold = oS.alphas[j].copy();
        if (oS.labelMat[i] != oS.labelMat[j]):
            L = max(0, oS.alphas[j] - oS.alphas[i])
            H = min(oS.C, oS.C + oS.alphas[j] - oS.alphas[i])
        else:
            L = max(0, oS.alphas[j] + oS.alphas[i] - oS.C)
            H = min(oS.C, oS.alphas[j] + oS.alphas[i])
        if L==H: print ("L==H"); return 0
        eta = 2.0 * oS.K[i,j] - oS.K[i,i] - oS.K[j,j] #changed for kernel
        if eta >= 0: print ("eta>=0"); return 0
        oS.alphas[j] -= oS.labelMat[j]*(Ei - Ej)/eta
        oS.alphas[j] = clipAlpha(oS.alphas[j],H,L)
        updateEk(oS, j) #added this for the Ecache
        if (abs(oS.alphas[j] - alphaJold) < 0.00001): print ("j not moving enough"); return 0
        oS.alphas[i] += oS.labelMat[j]*oS.labelMat[i]*(alphaJold - oS.alphas[j])#update i by the same amount as j
        updateEk(oS, i) #added this for the Ecache                    #the update is in the oppostie direction
        b1 = oS.b - Ei- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.K[i,i] - oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.K[i,j]
        b2 = oS.b - Ej- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.K[i,j]- oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.K[j,j]
        if (0 < oS.alphas[i]) and (oS.C > oS.alphas[i]): oS.b = b1
        elif (0 < oS.alphas[j]) and (oS.C > oS.alphas[j]): oS.b = b2
        else: oS.b = (b1 + b2)/2.0
        return 1
    else: return 0

def smoP(dataMatIn, classLabels, C, toler, maxIter,kTup=('lin', 0)):    #full Platt SMO
    oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler, kTup)
    iter = 0
    entireSet = True; alphaPairsChanged = 0
    while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)):
        alphaPairsChanged = 0
        if entireSet:   #go over all
            for i in range(oS.m):
                alphaPairsChanged += innerL(i,oS)
                print ("fullSet, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged))
            iter += 1
        else:#go over non-bound (railed) alphas
            nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0]
            for i in nonBoundIs:
                alphaPairsChanged += innerL(i,oS)
                print ("non-bound, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged))
            iter += 1
        if entireSet: entireSet = False #toggle entire set loop
        elif (alphaPairsChanged == 0): entireSet = True
        print ("iteration number: %d" % iter)
    return oS.b,oS.alphas

def calcWs(alphas,dataArr,classLabels):
    X = mat(dataArr); labelMat = mat(classLabels).transpose()
    m,n = shape(X)
    w = zeros((n,1))
    for i in range(m):
        w += multiply(alphas[i]*labelMat[i],X[i,:].T)
    return w

def testRbf(k1=1.3):
    dataArr,labelArr = loadDataSet('testSetRBF.txt')
    b,alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, ('rbf', k1)) #C=200 important
    datMat=mat(dataArr); labelMat = mat(labelArr).transpose()
    svInd=nonzero(alphas.A>0)[0]
    sVs=datMat[svInd] #get matrix of only support vectors
    labelSV = labelMat[svInd];
    print ("there are %d Support Vectors" % shape(sVs)[0])
    m,n = shape(datMat)
    errorCount = 0
    for i in range(m):
        kernelEval = kernelTrans(sVs,datMat[i,:],('rbf', k1))
        predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b
        if sign(predict)!=sign(labelArr[i]): errorCount += 1
    print ("the training error rate is: %f" % (float(errorCount)/m))
    dataArr,labelArr = loadDataSet('testSetRBF2.txt')
    errorCount = 0
    datMat=mat(dataArr); labelMat = mat(labelArr).transpose()
    m,n = shape(datMat)
    for i in range(m):
        kernelEval = kernelTrans(sVs,datMat[i,:],('rbf', k1))
        predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b
        if sign(predict)!=sign(labelArr[i]): errorCount += 1
    print ("the test error rate is: %f" % (float(errorCount)/m) )

#加载图像
def loadImages(dirName):
    from os import listdir
    hwLabels = []
    trainingFileList = listdir(dirName)           #load the training set
    m = len(trainingFileList)
    trainingMat = zeros((m,1024))
    for i in range(m):
        fileNameStr = trainingFileList[i]
        fileStr = fileNameStr.split('.')[0]     #take off .txt
        classNumStr = int(fileStr.split('_')[0])
        if classNumStr == 9: hwLabels.append(-1)
        else: hwLabels.append(1)
        trainingMat[i,:] = img2vector('%s/%s' % (dirName, fileNameStr))
    return trainingMat, hwLabels

#将图像转换为向量
def img2vector(filename):
    returnVect = zeros((1,1024))
    fr = open(filename)
    for i in range(32):
        lineStr = fr.readline()
        for j in range(32):
            returnVect[0,32*i+j] = int(lineStr[j])
    return returnVect

#利用svm测试数字
def testDigits(kTup=('rbf', 10)):
    #训练集
    dataArr,labelArr = loadImages('trainingDigits')  #加载数据集，标签
    b,alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, kTup) #使用smo算法计算b，alphas
    datMat=mat(dataArr); labelMat = mat(labelArr).transpose()    #转换成mat格式
    svInd=nonzero(alphas.A>0)[0]
    sVs=datMat[svInd]
    labelSV = labelMat[svInd];
    print ("there are %d Support Vectors" % shape(sVs)[0])
    m,n = shape(datMat)
    errorCount = 0
    for i in range(m):
        kernelEval = kernelTrans(sVs,datMat[i,:],kTup)
        predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b
        if sign(predict)!=sign(labelArr[i]): errorCount += 1
    print ("the training error rate is: %f" % (float(errorCount)/m))
    #测试集
    dataArr,labelArr = loadImages('testDigits')
    errorCount = 0
    datMat=mat(dataArr); labelMat = mat(labelArr).transpose()
    m,n = shape(datMat)
    for i in range(m):
        kernelEval = kernelTrans(sVs,datMat[i,:],kTup)
        predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b
        if sign(predict)!=sign(labelArr[i]): errorCount += 1
    print ("the test error rate is: %f" % (float(errorCount)/m) )

#测试
testDigits(kTup=('rbf', 10))

'''#######********************************
Non-Kernel VErsions below
'''#######********************************

class optStructK:
    def __init__(self,dataMatIn, classLabels, C, toler):  # Initialize the structure with the parameters
        self.X = dataMatIn
        self.labelMat = classLabels
        self.C = C
        self.tol = toler
        self.m = shape(dataMatIn)[0]
        self.alphas = mat(zeros((self.m,1)))
        self.b = 0
        self.eCache = mat(zeros((self.m,2))) #first column is valid flag

def calcEkK(oS, k):
    fXk = float(multiply(oS.alphas,oS.labelMat).T*(oS.X*oS.X[k,:].T)) + oS.b
    Ek = fXk - float(oS.labelMat[k])
    return Ek

def selectJK(i, oS, Ei):         #this is the second choice -heurstic, and calcs Ej
    maxK = -1; maxDeltaE = 0; Ej = 0
    oS.eCache[i] = [1,Ei]  #set valid #choose the alpha that gives the maximum delta E
    validEcacheList = nonzero(oS.eCache[:,0].A)[0]
    if (len(validEcacheList)) > 1:
        for k in validEcacheList:   #loop through valid Ecache values and find the one that maximizes delta E
            if k == i: continue #don't calc for i, waste of time
            Ek = calcEk(oS, k)
            deltaE = abs(Ei - Ek)
            if (deltaE > maxDeltaE):
                maxK = k; maxDeltaE = deltaE; Ej = Ek
        return maxK, Ej
    else:   #in this case (first time around) we don't have any valid eCache values
        j = selectJrand(i, oS.m)
        Ej = calcEk(oS, j)
    return j, Ej

def updateEkK(oS, k):#after any alpha has changed update the new value in the cache
    Ek = calcEk(oS, k)
    oS.eCache[k] = [1,Ek]

def innerLK(i, oS):
    Ei = calcEk(oS, i)
    if ((oS.labelMat[i]*Ei < -oS.tol) and (oS.alphas[i] < oS.C)) or ((oS.labelMat[i]*Ei > oS.tol) and (oS.alphas[i] > 0)):
        j,Ej = selectJ(i, oS, Ei) #this has been changed from selectJrand
        alphaIold = oS.alphas[i].copy(); alphaJold = oS.alphas[j].copy();
        if (oS.labelMat[i] != oS.labelMat[j]):
            L = max(0, oS.alphas[j] - oS.alphas[i])
            H = min(oS.C, oS.C + oS.alphas[j] - oS.alphas[i])
        else:
            L = max(0, oS.alphas[j] + oS.alphas[i] - oS.C)
            H = min(oS.C, oS.alphas[j] + oS.alphas[i])
        if L==H: print ("L==H"); return 0
        eta = 2.0 * oS.X[i,:]*oS.X[j,:].T - oS.X[i,:]*oS.X[i,:].T - oS.X[j,:]*oS.X[j,:].T
        if eta >= 0: print ("eta>=0"); return 0
        oS.alphas[j] -= oS.labelMat[j]*(Ei - Ej)/eta
        oS.alphas[j] = clipAlpha(oS.alphas[j],H,L)
        updateEk(oS, j) #added this for the Ecache
        if (abs(oS.alphas[j] - alphaJold) < 0.00001): print ("j not moving enough"); return 0
        oS.alphas[i] += oS.labelMat[j]*oS.labelMat[i]*(alphaJold - oS.alphas[j])#update i by the same amount as j
        updateEk(oS, i) #added this for the Ecache                    #the update is in the oppostie direction
        b1 = oS.b - Ei- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.X[i,:]*oS.X[i,:].T - oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.X[i,:]*oS.X[j,:].T
        b2 = oS.b - Ej- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.X[i,:]*oS.X[j,:].T - oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.X[j,:]*oS.X[j,:].T
        if (0 < oS.alphas[i]) and (oS.C > oS.alphas[i]): oS.b = b1
        elif (0 < oS.alphas[j]) and (oS.C > oS.alphas[j]): oS.b = b2
        else: oS.b = (b1 + b2)/2.0
        return 1
    else: return 0

def smoPK(dataMatIn, classLabels, C, toler, maxIter):    #full Platt SMO
    oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler)
    iter = 0
    entireSet = True; alphaPairsChanged = 0
    while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)):
        alphaPairsChanged = 0
        if entireSet:   #go over all
            for i in range(oS.m):
                alphaPairsChanged += innerL(i,oS)
                print ("fullSet, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged))
            iter += 1
        else:#go over non-bound (railed) alphas
            nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0]
            for i in nonBoundIs:
                alphaPairsChanged += innerL(i,oS)
                print ("non-bound, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged))
            iter += 1
        if entireSet: entireSet = False #toggle entire set loop
        elif (alphaPairsChanged == 0): entireSet = True
        print ("iteration number: %d" % iter)
    return oS.b,oS.alphas


class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
        from collections import Counter
        n = len(s)
        l,r = 0,0
        counter = Counter()
        ans = 0
        while r < n:
            counter[s[r]] += 1
            while r-l+1 - counter.most_common(1)[0][1] > k:
                counter[s[l]] -= 1
                l += 1
            ans = max(ans,r-l+1)
            r += 1
        return ans


#!/usr/bin/.env python
import dataset
from bot.database import user_table, DBUser, get_db_user

db_sql = dataset.connect("sqlite:////home/max/test/db.sqlite3")

print(db_sql.tables)
for x in db_sql["oauth_record"].all():
    if x and x.get("discord_user_id"):
        db_user = DBUser(discord_id = x.get("discord_user_id"), registered=True, registration_step=5, choate_email=x.get("school_email"), first_name = x.get("first_name"), last_name = x.get("last_name"), opt_out_email = x.get("opt_out_email"))
        user_table.create(db_user)


from expenses import Expense
import datetime
import calendar

# creating main functions here so that when the file runs- all these functions run as well
def main():
    print(f" 🎯 Running Expense Tracker!")

    # creating a variable to store csv file name
    expense_file_path = "expenses.csv"

    # get an input from the user for their budget in this month
    budget = float(input("Enter your this months budget : "))

    # This function is to - Get user input for expense
    # for that create a function
    expense = get_user_expense()    # this function will return user expense which he has entered below and this "return" will be stored in "expense" variable

    # This function is to - write their expense to file
    save_expense_to_file(expense, expense_file_path)

    # This function is to - Read file and sumarize expenses.
    sumarize_expenses(expense_file_path, budget)

def get_user_expense():
    print(f" 📝 Getting User expense")
    # get all the necessary inputs from the user
    expense_name = input("Enter expense name - ")
    expense_amount = float(input("Enter expense amount - "))
    print(f"you have entered {expense_name}, {expense_amount}")
    # let user select the category of the expense
    expense_categories = [
        "🍔 Food", "🏠 Home", "💻 Work", "🎭 Fun", "Ⓜ Misc"
    ]

    while True:
        # display the categorys available to choose from
        print("Select a category: ")
        for i, category_name in enumerate(expense_categories):
            print(f" {i+1},  {category_name}")    # +1 is done because list indexing starts from 0

        value_range = f"1 - {len(expense_categories)}"       # presenting the value range to choose from

        # store the category in a variable and subtract it from 1 to get the programming index
        selected_index = int(input(f"Enter a category number from range {value_range}: ")) - 1

        # solving a possible test case if the user enters input out of the range
        if selected_index in range(len(expense_categories)):
            selected_category = expense_categories[selected_index]
            new_expense = Expense(name = expense_name, category = selected_category, amount = expense_amount)    #creating an object called "expense" to store the info

            return new_expense
        else:
            print("The number entered is invalid please enter a number from the range!")
        break

def save_expense_to_file(expense : Expense, expense_file_path):
    print(f" 🎯 Saving User expense : {expense} to {expense_file_path}")
    with open(expense_file_path, "a", newline='', encoding='utf-8') as f:       # utf-8 is used because open() does not support unicode character encoding. Therefore specifying the utf-8 encoding
        f.write(f"{expense.name},{expense.amount},{expense.category}\n")

def sumarize_expenses(expense_file_path, budget):
    # get the current date
    now = datetime.datetime.now()

    # Get the number of days in the current month
    days_in_month = calendar.monthrange(now.year, now.month)[1]

    # Now calculate the remaining days
    remaining_days = days_in_month - now.day

    print("\n")
    print(f" 🎯 Summarizing User expense")
    # creating an empty list "expenses" to store objects created using the "Expense" class.
    expenses: list[Expense] = []
    with open(expense_file_path, "r", encoding="utf-8") as f:
        lines = f.readlines()       # reading all the lines in the csv
        for line in lines:
            stripped_line = line.strip()       #removing the white spaces which might be leading and trailing
            expense_name, expense_amount, expense_category = stripped_line.split(",")      # splitting all the variables using split function by using "," and returns a list and stores it in the variables as mentioned above
            line_expenses = Expense(name = expense_name, amount = float(expense_amount), category = expense_category)
            # print(line_expenses)
            expenses.append(line_expenses)

    # now calculate the total amounts by category
    amount_by_category = {}
    for expense in expenses:
        key = expense.category   # this "expense" variable created here will a ccess the list "expenses" where all the line by line expense are appended into the list
        # The "expense" object here is from the class "Expense", and we are trying to access the "category" instance of that object. Hence, "expense.category"
        if key in amount_by_category:
            amount_by_category[key] += expense.amount      # we are using the actual "expense" object and not the variable "expense_amount"
        else:
            amount_by_category[key] = expense.amount

    # printing the total expense by category
    print("Expenses by Category 📈")
    for key, amount in amount_by_category.items():
        print(f"  {key} : $ {amount:.2f}")

    total_spent = sum([x.amount for x in expenses])
    print(f"You've spent ${total_spent:.2f} this month")
    remaining_budegt = budget - total_spent
    if(remaining_budegt < 0):
        print(red(f"💸 You've exceeded your this month's budget of 💲 {budget:.2f} by: 💲 {total_spent - budget}"))
    else :
        print(green(f"💸 Your remaining budget is 💲{remaining_budegt:.2f}"))
        print(green(f"Remaining days in the current month:  {remaining_days}"))

        daily_budget = remaining_budegt / remaining_days
        print(green(f"👉 Budget per day : ${daily_budget:.2f}"))

# Now if I have to make the amounts to stand out and showcase them in colour then I can do that by using this function
def green(text) :
    return f"\033[92m{text}\033[0m"
def red(text) :
    return f"\033[91m{text}\033[0m"

if __name__ == "__main__":
    main()


"""create user marsavin table

Revision ID: 7d3575c1c41b
Revises: ddbfb0dc008d
Create Date: 2020-03-18 10:31:33.687075

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from ckan.migration import skip_based_on_legacy_engine_version

# revision identifiers, used by Alembic.
revision = '7d3575c1c41b'
down_revision = 'ddbfb0dc008d'
branch_labels = None
depends_on = None

def upgrade():
    if skip_based_on_legacy_engine_version(op, __name__):
        return
    op.create_table('user_marsavin',
                    sa.Column('id', postgresql.UUID(),
                              server_default=sa.text('uuid_generate_v4()')),
                    sa.Column('user_id', sa.Text),
                    sa.Column('allow_marketting_emails', sa.Text)
                    )

def downgrade():
    op.drop_table("user_marsavin")