0

Doing Roman Number converter kata using Bellwares SpecUnit

by klh 17. marts 2009 03:53

I was reading Scott Bellwares article on Behavior-Driven Development, and was inspired to try out his framework called specunit-net"

So I took a kata from tddproblems and tried it out, I took the Roman number conversion problem:

So first I implemented a simple spec base class:

[TestFixture]
    [Concern("base test")]
    public abstract class behaves_like_context_with_Converter : ContextSpecification
    {
        protected RomanNumberConverter _converter;

        protected override void Context()
        {
            _converter = new RomanNumberConverter();
        }

        protected void ShouldReturn(string romanNumber, int integerNumber)
        {
            int result;
            result = _converter.Convert(romanNumber);
            result.ShouldEqual(integerNumber);
        }
    }

 

Then I added specs for one char at a time, I started with I (one) and moved up, but reSharper re-structures this stuff:

[Concern("passed only one character")]
    public class when_passed_one_character : behaves_like_context_with_Converter
    {
        [Observation]
        [Test]
        public void should_return_fifty()
        {
            ShouldReturn("L", 50);
        }

        [Observation]
        [Test]
        public void should_return_five()
        {
            ShouldReturn("V", 5);
        }

        [Observation]
        [Test]
        public void should_return_five_hundred()
        {
            ShouldReturn("D", 500);
        }

        [Observation]
        [Test]
        public void should_return_one()
        {
            ShouldReturn("I", 1);
        }

        [Observation]
        [Test]
        public void should_return_one_hundred()
        {
            ShouldReturn("C", 100);
        }

        [Observation]
        [Test]
        public void should_return_one_thousand()
        {
            ShouldReturn("M", 1000);
        }

        [Observation]
        [Test]
        public void should_return_ten()
        {
            ShouldReturn("X", 10);
        }
    }

So I started with the simplest implementation to make it work (unfortunately I didn’t save all the intermediate states so I can only show the end result), and followed that pragma all the way through, only implementing the simplest possible code to make it work.

Here is the end result, I cannot say that it is perfect, but i behaves like the spec says it should:

public class RomanNumberConverter
    {
        public int Convert(string s)
        {
            s = s.Trim();
            int result = 0;
            for (int position = 0; position < s.Length; position++)
            {
                if (position > 0 && !IsPreviousCharSmallerThanOrEqualToCurrent(s, position))
                {
                    result += ConvertOneCharacter(s[position]) - ConvertOneCharacter(s[position - 1]);
                    result -= ConvertOneCharacter(s[position - 1]);
                }
                else
                    result += ConvertOneCharacter(s[position]);
            }   
            return result;
        }

        private bool IsPreviousCharSmallerThanOrEqualToCurrent(string s, int position)
        {
            char previous = s[position - 1];
            char current = s[position];
            if (ConvertOneCharacter(previous) >= ConvertOneCharacter(current))
                return true;
            return false;
        }

        public int ConvertOneCharacter(char s)
        {
            Dictionary<char, int> romanValues = new Dictionary<char, int>
                                                    {
                                                        {'I', 1},
                                                        {'V', 5},
                                                        {'X', 10},
                                                        {'L', 50},
                                                        {'C', 100},
                                                        {'D', 500},
                                                        {'M', 1000}
                                                    };
            if (romanValues.ContainsKey(s))
            {
                return romanValues[s];
            }
            throw new ArgumentException(s + " is not a roman number");
        }
    }

At the bottom you find all the rest of the specs.

 

 

 [Concern("passed only two simple characters")]
    public class when_passed_two_simple_character : behaves_like_context_with_Converter
    {
        [Observation]
        [Test]
        public void should_return_II()
        {
            ShouldReturn("II", 2);
        }

        [Observation]
        [Test]
        public void should_return_XV()
        {
            ShouldReturn("XV", 15);
        }
    }

    [Concern("passed only three simple characters")]
    public class when_passed_three_simple_character : behaves_like_context_with_Converter
    {
        [Observation]
        [Test]
        public void should_return_XVI()
        {
            ShouldReturn("XVI", 16);
        }
    }

    [Concern("passed only values below ten")]
    public class when_passed_values_below_ten : behaves_like_context_with_Converter
    {
        [Observation]
        [Test]
        public void should_return_IV()
        {
            ShouldReturn("IV", 4);
        }

        [Observation]
        [Test]
        public void should_return_IX()
        {
            ShouldReturn("IX", 9);
        }


        [Observation]
        [Test]
        public void should_return_VI()
        {
            ShouldReturn("VI", 6);
        }

        [Observation]
        [Test]
        public void should_return_VIII()
        {
            ShouldReturn("VIII", 8);
        }
    }

    [Concern("passed only one characterwhen passed spaces")]
    public class when_passed_spaces : behaves_like_context_with_Converter
    {
        [Observation]
        [Test]
        public void should_return_trim()
        {
            ShouldReturn("II ", 2);
        }
    }

    [Concern("when passed invalid character")]
    public class when_passed_invalid_character : behaves_like_context_with_Converter
    {
        [Observation]
        [Test]
        [ExpectedException(typeof (ArgumentException))]
        public void should_throw()
        {
            _converter.Convert("P");
        }
    }

    [Concern("when passed large numbers")]
    public class when_passed_large_numbers : behaves_like_context_with_Converter
    {
        [Observation]
        [Test]
        public void should_return_1900()
        {
            ShouldReturn("MCM ", 1900);
        }

        [Observation]
        [Test]
        public void should_return_1974()
        {
            ShouldReturn("MCMLXXIV", 1974);
        }

        [Observation]
        [Test]
        public void should_return_1998()
        {
            ShouldReturn("MCMXCVIII", 1998);
        }

        [Observation]
        [Test]
        public void should_return_2008()
        {
            ShouldReturn("MMVIII", 2008);
        }
    }

Tags:

0

[DK] Teknisk gæld forklaret for CIO/CEO

by klh 14. marts 2009 14:02

Det kan tit være svært at overbevise Chefer, projektledere om at det at lave god kode kvalitet er en god idé, derfor kan det være en god idé at bruge en analogi:

For at skabe omsætning kan man stifte gæld, forudsat at omsætningen giver afkast og at afkastet over stiger gælden + renterne.

Hvis gælden + renterne overstiger afkastet så giver det underskud.

Det er det samme i software udvikling:

Vi kan godt skynde os at lave noget for at få hurtigere resultat og det er også nødvendigt til tider. Men hvis vi ikke betaler af på gælden så vil den overvælde os og gøre at vi ikke kan bevæge os hurtigt.

Måske vil den endda gøre at vi slet ikke kan bevæge os.

Derfor er det bedre at mindske gælden "on the fly" mens man producerer softwaren.

Det er med at balancere sin gæld og renter med omsætningen og afkastet.

 

I IT-Factory valgte man at stifte gæld for at betale af på sin gæld.

Man startede med at stifte en lille gæld og inden den skulle betales, stiftede man en lidt større gæld for at betale den første gæld med renter.

Sådan fortsatte det i en ond cirkel, for til sidst at ende i konkurs.

Vi skulle nødigt lave en "Stein Bagger" på vores software!

 

Vedligeholdet er renterne

Gælden er kvaliteten af koden

Inspireret meget kriaftigt af Mark Nijhofs mail på software_craftmanship maillinglisten.

Tags:

0

[DK]Uncle Bob om software projekter (min formulering)

by klh 11. marts 2009 04:33

Uncle Bob om software projekter:
"Tit haster vi igennem for at blive færdige til tiden, og sjusker for at gøre det hurtigere.

Men når vi sjusker os igennem opdager vi tit,  at det sjusk vi lavede for at blive hurtigt færdig,

er det der forsinker os og gør at vi bliver forsinkede."

 

Tags:

generelt | software

0

[DK]Sådan forstår jeg Udi Dahan's "Intentions & Interfaces - Making Patterns Concrete" fra TechEd

by klh 2. marts 2009 09:02

Hvis jeg  skal hente data fra en database i forbindelse med en ordre, så kan jeg i nogle tilfælde have brug for at hente ordren med ordrelinjer, og i nogle tilfælde kan jeg have brug for kun at hente ordren selv.

I stedet for at hardcode dette i forretningslaget eller i repository, kan jeg lægge det ud for sig selv.

Bemærk venligst at dette er mit forsøg på at forstå en metode, det er ikke "production ready"

Det gør jeg ved først at definerer et interface:

public interface IFetchingStrategy<T> where T : IEntity
{
    void Fetch();
}

Dette interface tager så en IEntity, som er rollen.

Rollen kan være IOrder (hent kun ordren) eller IOrderCalculator(Hent også ordrelinjer) eller IOrderInfo(Hent ordren i readonly tilstand)

public interface IOrder : IEntity
{
}
public interface IOrderCalculator : IEntity
{
    Money CalculateCost();
}

public interface IOrderInfo : IEntity
{
}

Nede i mit repository spørger jeg min DI container om en FetchingStrategy

class Repository<T> where T : IEntity
{
  public static T FindOne(int id)
  {
    IFetchingStrategy<T> fetchingStrategy =  ResolveType.Of<IFetchingStrategy<T>>();
    fetchingStrategy.Fetch();
    return default(T);
  }
}

Alt efter typen af T, får jeg en forskellig fetching strategy:

public class OrderService
{
    public virtual Money CalculateCostForOrder(int orderId)
    {
        IOrderCalculator order = Repository<IOrderCalculator>.FindOne(orderId);
        return new Money();
    }

    public virtual void DisplayOrder(int orderId)
    {
        IOrderInfo order = Repository<IOrderInfo>.FindOne(orderId);
    }

    public void LoadOrder(int orderId)
    {
        IOrder order = Repository<IOrder>.FindOne(orderId);
    }
}

Disse fetching strategier har jeg lagt ud for sig, så de kan ændres flydende (Open / Closed principle)

public class OrderCalculatorFetchingStrategy : IFetchingStrategy<IOrderCalculator>
{
    public void Fetch()
    {
        Console.WriteLine("Fetch the whole bunch - do eager loading");
    }
}
public class OrderFetchingStrategy : IFetchingStrategy<IOrder>
{
    public void Fetch()
    {
        Console.WriteLine("Fetch only the order");
    }
}
public class OrderInfoFetchingStrategy : IFetchingStrategy<IOrderInfo>
{
    public void Fetch()
    {
        Console.WriteLine("Fetch in read-only mode");
    }
}

Her konfigurerer jeg min DI Container:

IWindsorContainer container = new WindsorContainer();
container.AddComponent("OrderCalculatorFetchingStrategy", 
                typeof(IFetchingStrategy<IOrderCalculator>),
                typeof(OrderCalculatorFetchingStrategy));
container.AddComponent("OrderFetchingStrategy", 
                typeof(IFetchingStrategy<IOrder>),
                typeof(OrderFetchingStrategy));
container.AddComponent("OrderInfoFetchingStrategy", 
                typeof(IFetchingStrategy<IOrderInfo>),
                typeof(OrderInfoFetchingStrategy));
ResolveType.Initialize(container); 

Kort sagt kan man vel sige at det er en implementation af Strategypattern, hvor man bruger sin DI container til at hente strategien på runtime.

Jeg vil gerne henvise til Udi Dahans blogposts om emnet:

Fetching Stratey Design

Better Domain Driven Design implementation

Ayende har også en god blogpost om emnet:

Adaptive Domain models with Rhino Commons

Tags:

0

Just discovered Shelfari

by klh 17. februar 2009 15:08

I just discovered shelfari.com

It is a place to keep all your book information, like which books have you read, which do you like.

It's really slick and looks nice.

Here's my entry:

http://www.shelfari.com/khebbie

By the way I even added a widget to this blog, where you can see my books. I got the widget from http://www.dscoduc.com/post/2008/08/01/Shelfari-Widget-for-BlogEngineNET.aspx

0

Generating interfaces for legacy classes

by klh 13. februar 2009 09:11

From time to time you bump into legacy code, where somebody thought that static methods was the solution to all your problems. We all know that an architecture with static methods calling static methods is very tightly coupled, and we would like decouple it.

What I would like to do is make an interface, with all public methods, and make an adapter that  implements the interface and uses the static methods of the legacy class.

EDIT:

Of course I should mention that this is a Proof of Concept, hence the code is not complete nor perfect:

-  The generated code uses the long form of the built in types instead of the short form

- There are duplicates distributed all around the code

- The code does not work with out/ref parameters (there were no usage of the two in the code I needed code generation for)

- The classname, namespace and assemblylocation should have been T4 properties

This was just to put an example of T4 usage out.

So here is an example of a legacy class:

namespace t4ConsoleApp
{
class StaticStuff
{
public static string Test()
{
return "test";
}
public static int GetNumber()
{
return 42;
}
public static bool IsAbove42(int x)
{
if(x > 42)
return true;
return false;
}
}
}

 

So what i would like is something like this:

namespace t4ConsoleApp
{
public interface IStaticStuff
{
System.String Test();
System.Int32 GetNumber();
System.Boolean IsAbove42(System.Int32 x);
}
}
namespace t4ConsoleApp
{
public class StaticStuffAdapter : IStaticStuff
{
public System.String Test()
{
return StaticStuff.Test();
}
public System.Int32 GetNumber()
{
return StaticStuff.GetNumber();
}
public System.Boolean IsAbove42(System.Int32 x)
{
return StaticStuff.IsAbove42(x);
}
}
}

Creating this in a manual way could be big work, so I decided to do it using T4.

First I make a class that can do reflection over the legacy class:

using System;
using System.Text;
using System.Reflection;
namespace t4ConsoleApp
{
public class StaticReflection
{
public Type GetType(string assemblyLocation, string typeName)
{
Assembly assembly = Assembly.LoadFrom(assemblyLocation);
if (assembly == null)
{
throw new Exception("assembly not found");
}
Type tp = assembly.GetType(typeName);
if (tp == null)
throw new Exception("type not found");
return tp;
}
public ClassInfoValue GetAllPublicStaticMembers(Type mytype)
{
ClassInfoValue classInfoValue = new ClassInfoValue(7);
MethodInfo[] miArray = mytype.GetMethods(BindingFlags.Public 
| BindingFlags.Static);
foreach (MethodInfo mi in miArray)
{
string parameterNames = GetParameterNames(mi);
string parametersWithNames = GetParametersWithNames(mi);
classInfoValue.ParameterNamesList.Add(parameterNames);
classInfoValue.Returntypes.Add(mi.ReturnType.ToString());
classInfoValue.MethodNames.Add(mi.Name);
classInfoValue.ParameterswithNamesList.Add(parametersWithNames);
}
return classInfoValue;
}
private static string GetParameterNames(MethodInfo mi)
{
ParameterInfo[] piArray = mi.GetParameters();
StringBuilder sb = new StringBuilder();
int i = 0;
foreach (ParameterInfo info in piArray)
{
if (i == 0)
{
sb.Append(info.Name);
}
else
{
sb.Append(", " + info.Name);
}
i++;
}
return sb.ToString();
}
private static string GetParametersWithNames(MethodInfo mi)
{
ParameterInfo[] piArray = mi.GetParameters();
StringBuilder sb = new StringBuilder();
int i = 0;
foreach (ParameterInfo info in piArray)
{
if (i == 0)
{
sb.Append(info.ParameterType + " " + info.Name);
}
else
{
sb.Append(", " + info.ParameterType + " " + info.Name);
}
i++;
}
return sb.ToString();
}
}
}

And lastly the T4 template to do this, I made a console app to contain the T4 stuff

<#@ template language="C#v2.0" hostspecific="True" #>
<#@ output extension=".generated.cs" #>
<#@ assembly name="system.dll" #>
<#@ assembly name="D:\Src\t4consoleapp\t4consoleapp\bin\debug\t4ConsoleApp.exe" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="t4ConsoleApp" #>
<#
string classname = "StaticStuff";
string nameSpace= "t4ConsoleApp";
string classNameWithNamespace = nameSpace + "." + classname;
string assemblylocation =  @"D:\Src\t4consoleapp\t4consoleapp\bin\debug\t4ConsoleApp.exe";
StaticReflection staticReflection = new StaticReflection();
Type myType = staticReflection.GetType(assemblylocation, classNameWithNamespace);
ClassInfoValue classInfo = staticReflection.GetAllPublicStaticMembers(myType);
#>
<#= "//Generated at: " + DateTime.Now #>
using System;
namespace <#= nameSpace #>
{
public interface I<#= classname #>
{
<# for(int i = 0; i< classInfo.MethodNames.Count; i++){ #>
<# if(classInfo.Returntypes[i] == typeof(void).ToString()) {#>
void <#= classInfo.MethodNames[i] #>(<#= classInfo.ParameterswithNamesList[i] #>);
<#} else {#>
<#= classInfo.Returntypes[i] #> <#= classInfo.MethodNames[i] #>(<#= classInfo.ParameterswithNamesList[i] #>);
<# } #>
<# } #>
}
}
namespace <#= nameSpace #>
{
public class <#= classname #>Adapter :I<#= classname #>
{
<# for(int i = 0; i< classInfo.MethodNames.Count; i++){ #>
<# if(classInfo.Returntypes[i] == typeof(void).ToString()) {#>
public void <#= classInfo.MethodNames[i] #>(<#= classInfo.ParameterswithNamesList[i] #>)
{
<#= classname #>.<#= classInfo.MethodNames[i] #>(<#= classInfo.ParameterNamesList[i] #>);
}
<#} else {#>
public <#= classInfo.Returntypes[i] #> <#= classInfo.MethodNames[i] #>(<#= classInfo.ParameterswithNamesList[i] #>)
{
return <#= classname #>.<#= classInfo.MethodNames[i] #>(<#= classInfo.ParameterNamesList[i] #>);
}
<# } #>
<# } #>
}
}
namespace <#= nameSpace #>
{
public class <#= classname #>Factory
{
public I<#= classname #> Get<#= classname #>()
{
return new <#= classname #>Adapter();
}
}
}

0

Currying in C#

by klh 7. januar 2009 06:12

I recently listend to Deep Fried Bytes on functional programming in C#.

In the show notes, there is a code sample of doing curring in C#, I worked a bit further on it.

This is what came out of it:

First I did a delegate, that would take a delegate:

Func<Func<int, int, int>, int, Func<int, int>> MyCurry =                
(dele, x) => z => dele(z, x);

Then I made three functions, that would adhere to Func<int, int, int> :

Func<int, int, int> add = (x, y) => x + y;
Func<int, int, int> sub = (x, y) => x - y;
Func<int, int, int> mul = (x, y) => x * y;

Then I made three curried functions, that used the first delegate:

var add9 = MyCurry(add, 9);
var sub9 = MyCurry(sub, 9);
var mulBy2 = MyCurry(mul, 2);

And lastly I used the curried functions:

int result = add9(5);
result = sub9(10);
result = mulBy2(5);

And it actually works!

This way it is really easy to curry functions together…

  kick it on DotNetKicks.com

Tags:

0

Merry Christmas

by klh 22. december 2008 08:50

Well Merry Christmas!

o<:-)

Tags:

0

[danish] ironRuby BDD test af JuleGolf

by klh 11. december 2008 15:15

Jeg har lavet en BDD style ironRuby test af Julegolf konkurrencen på kodehoved.dk: 

require 'test'
require 'c:\temp\JuleGolfLib.dll'
Nsp = JuleGolfLib

describe "Jule Golf"  do
  it "should return integer when given 'en' and 'et'" do
      juleGolf = Nsp::TTN
      juleGolf.TextToNumber("et").should == 1;
      juleGolf.TextToNumber("en").should == 1;
   end
  
    it "should return integer when given numbers below 10 and above 1" do
      juleGolf = Nsp::TTN
      juleGolf.TextToNumber("to").should == 2;
      juleGolf.TextToNumber("tre").should == 3;
      juleGolf.TextToNumber("fire").should == 4;
      juleGolf.TextToNumber("fem").should == 5;
      juleGolf.TextToNumber("seks").should == 6;
      juleGolf.TextToNumber("syv").should == 7;
      juleGolf.TextToNumber("otte").should == 8;
      juleGolf.TextToNumber("ni").should == 9;
     
   end
  
   it "should return integer when given numbers from the 10 tabel" do
      juleGolf = Nsp::TTN
      juleGolf.TextToNumber("ti").should == 10;
      juleGolf.TextToNumber("tyve").should == 20;
      juleGolf.TextToNumber("tredive").should == 30;
      juleGolf.TextToNumber("fyrre").should == 40;
      juleGolf.TextToNumber("halvtreds").should == 50;
      juleGolf.TextToNumber("tres").should == 60;
      juleGolf.TextToNumber("halvfjerds").should == 70;
      juleGolf.TextToNumber("firs").should == 80;
      juleGolf.TextToNumber("halvfems").should == 90;
   end
  
      it "should return integer when given the teens" do
        juleGolf = Nsp::TTN
      juleGolf.TextToNumber("elleve").should == 11;
      juleGolf.TextToNumber("tolv").should == 12;
      juleGolf.TextToNumber("tretten").should == 13;
      juleGolf.TextToNumber("fjorten").should == 14;
      juleGolf.TextToNumber("femten").should == 15;
      juleGolf.TextToNumber("seksten").should == 16;
      juleGolf.TextToNumber("sytten").should == 17;
      juleGolf.TextToNumber("atten").should == 18;
      juleGolf.TextToNumber("nitten").should == 19;
   end
  
    it "should return integer when given random numbers below 100" do
        juleGolf = Nsp::TTN
      juleGolf.TextToNumber("niogtyve").should == 29;
      juleGolf.TextToNumber("toogtredive").should == 32;
      juleGolf.TextToNumber("femoghalvtreds").should == 55;
      juleGolf.TextToNumber("toogfyrre").should == 42;
      juleGolf.TextToNumber("niogtredive").should == 39;
      juleGolf.TextToNumber("toogtres").should == 62;
      juleGolf.TextToNumber("fireoghalvfjerds").should == 74;
      juleGolf.TextToNumber("enogfirs").should == 81;
      juleGolf.TextToNumber("nioghalvfems").should == 99;
  end
 
  it "should be able to test through static methods" do
      juleGolf = Nsp::TTN
      juleGolf.TextToNumber("niogtyve").should == 29;
      end
end

 

Test biblioteket ser sådan ud:

# no prefix or _ local variable
# $ global variable
# @ instance variable
# @@ class (static) variable

$examples = 0
$messages = []

class PositiveExpectation
  def initialize(obj)
    @obj = obj
  end
 
  def ==(other)
    $examples += 1
    if @obj != other
      $messages << "Want #{other.inspect} got #{@obj.inspect}"
      print "E "
    else
      print "OK "
    end
    end
end

class Object
  def should
    PositiveExpectation.new(self)
  end
end

def it(description)
  print "\n it #{description}: "
  yield
end

def describe(description)
  print "#{description}"
  yield
  puts "\nend\n"
end

at_exit do
  puts "#{$messages.length} / #{$examples} failed"
  if $messages.length > 0
    puts "Failures: "
    $messages.each { |m| puts "- #{m}"}
  end
end


 

Tags:

Ruby

0

Craftmanship over heroics

by klh 28. november 2008 03:14

I read a good blog post the other day (http://softwarecraftsmanship.oreilly.com/news/2008/8/8/uncle-bob-on-craftsmanship-at-agile-2008)

It was a comment on a statment by Uncle Bob:  "Craftmanship over crap", but in the above mentioned blog post Dave Hoover modified the statement to "Craftmanship over heroics".

 

I have really taken this statement in, since it speaks to me.

 

So let me explain to you what it means to me:

Imagine having a carpenter work on your roof, while he works for you, you think it takes a lot of time. And furthermore repairing the roof is rather expensive.

 

So you talk to the carpenter and tell him that its taking too much time and its too expensive.

He says: "OK, I'll help your out, I'll cut 2 days off and some materiel costs as well"

Great your are satisfied, you have a good skilled carpenter (a craftman you think) who suggest he will get finished earlier and at a lower price. He is a real hero in your eyes!

 

So he finishes, you pay and everyone is happy.

 

Now a few years later there's a big storm and its raining.

Suddenly water is dripping down in your livingroom and kitchen.

So you call the carpenter and ask him what is wrong.

He replies: "Oh!, don't you remember we cut off two days? Well I didn't put on any roofing underlay, since it would take me two days, and the material costs as well, don't you remember we agreed on that?".

Naturally at this stage your are stunned, you thought you were working with a professional (A craftman), and then he chooses to do something like that !(trying to be a hero, but now he's not!)

 

Well this is a very strong analogy to the business of IT, there are lots of things (actually most of the things we do) that our customers don't understand, and hence they try to push us and make us cut the price.

So the question is are we going to not put in the roofing under lay, nobody will know - well at least not right now.

But when you cut corners here you are going to end up with being in big trouble.

 

So choose craftmanship over heroics.

kick it on DotNetKicks.com

Tags:

generelt | software

Powered by BlogEngine.NET 1.4.5.0
Original Design by Laptop Geek, Adapted by onesoft