Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Korko
SecretSanta.fr
Commits
9af41332
Commit
9af41332
authored
Dec 21, 2016
by
Korko
Browse files
Change algorithm for randomization to optimize timing and memory
parent
f3ebd0af
Changes
11
Hide whitespace changes
Inline
Side-by-side
app/Facades/Solver.php
0 → 100644
View file @
9af41332
<?php
namespace
Korko\SecretSanta\Facades
;
use
Illuminate\Support\Facades\Facade
;
class
Solver
extends
Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected
static
function
getFacadeAccessor
()
{
return
'solver'
;
}
}
app/Http/Controllers/RandomFormController.php
View file @
9af41332
...
...
@@ -4,8 +4,8 @@ namespace Korko\SecretSanta\Http\Controllers;
use
Illuminate\Http\Request
;
use
Korko\SecretSanta\Http\Requests\RandomFormRequest
;
use
Korko\SecretSanta\Libs\Resolver
;
use
Mail
;
use
Solver
;
use
Sms
;
use
Statsd
;
...
...
@@ -20,7 +20,7 @@ class RandomFormController extends Controller
{
$participants
=
$this
->
getParticipants
(
$request
);
$hat
=
Res
olver
::
resolve
(
$participants
);
$hat
=
S
olver
::
one
(
$participants
,
array_column
(
$participants
,
'exclusions'
)
);
$this
->
sendMessages
(
$request
,
$participants
,
$hat
);
...
...
@@ -54,38 +54,39 @@ class RandomFormController extends Controller
protected
function
sendMessages
(
Request
$request
,
array
$participants
,
array
$hat
)
{
foreach
(
$hat
as
$santaIdx
=>
$target
Name
)
{
foreach
(
$hat
as
$santaIdx
=>
$target
Idx
)
{
$santa
=
$participants
[
$santaIdx
];
$target
=
$participants
[
$targetIdx
];
$this
->
sendMessage
(
$request
,
$santa
,
$target
Name
);
$this
->
sendMessage
(
$request
,
$santa
,
$target
);
}
}
protected
function
sendMessage
(
Request
$request
,
array
$santa
,
$target
Name
)
protected
function
sendMessage
(
Request
$request
,
array
$santa
,
array
$target
)
{
if
(
!
empty
(
$santa
[
'email'
]))
{
Statsd
::
gauge
(
'email'
,
'+1'
);
$this
->
sendMail
(
$santa
,
$target
Name
,
$request
->
input
(
'title'
),
$request
->
input
(
'contentMail'
));
$this
->
sendMail
(
$santa
,
$target
,
$request
->
input
(
'title'
),
$request
->
input
(
'contentMail'
));
}
if
(
!
empty
(
$santa
[
'phone'
]))
{
Statsd
::
gauge
(
'phone'
,
'+1'
);
$this
->
sendSms
(
$santa
,
$target
Name
,
$request
->
input
(
'contentSMS'
));
$this
->
sendSms
(
$santa
,
$target
,
$request
->
input
(
'contentSMS'
));
}
}
protected
function
sendMail
(
$santa
,
$target
Name
,
$title
,
$content
)
protected
function
sendMail
(
array
$santa
,
array
$target
,
$title
,
$content
)
{
$contentMail
=
str_replace
([
'{SANTA}'
,
'{TARGET}'
],
[
$santa
[
'name'
],
$target
N
ame
],
$content
);
$contentMail
=
str_replace
([
'{SANTA}'
,
'{TARGET}'
],
[
$santa
[
'name'
],
$target
[
'n
ame
'
]
],
$content
);
$contentMail
.
=
PHP_EOL
.
trans
(
'form.mail.post'
);
Mail
::
raw
(
$contentMail
,
function
(
$m
)
use
(
$santa
,
$title
)
{
$m
->
to
(
$santa
[
'email'
],
$santa
[
'name'
])
->
subject
(
$title
);
});
}
protected
function
sendSms
(
$santa
,
$target
Name
,
$content
)
protected
function
sendSms
(
array
$santa
,
array
$target
,
$content
)
{
$contentSms
=
str_replace
([
'{SANTA}'
,
'{TARGET}'
],
[
$santa
[
'name'
],
$target
N
ame
],
$content
);
$contentSms
=
str_replace
([
'{SANTA}'
,
'{TARGET}'
],
[
$santa
[
'name'
],
$target
[
'n
ame
'
]
],
$content
);
$contentSms
.
=
PHP_EOL
.
trans
(
'form.sms.post'
);
Sms
::
message
(
$santa
[
'phone'
],
$contentSms
);
}
...
...
app/Libs/Combinator.php
deleted
100644 → 0
View file @
f3ebd0af
<?php
namespace
Korko\SecretSanta\Libs
;
use
Closure
;
use
Traversable
;
class
Combinator
{
public
static
function
all
(
array
$elements
,
Closure
$validator
=
null
)
:
array
{
$combinations
=
[];
foreach
(
self
::
getGenerator
(
$elements
)
as
$combination
)
{
$combination
=
array_combine
(
$elements
,
$combination
);
if
(
isset
(
$validator
)
&&
!
self
::
isCombinationValid
(
$combination
,
$validator
))
{
continue
;
}
$combinations
[]
=
$combination
;
}
return
$combinations
;
}
// With php7.1, return should be "?array" to return null in case of error
public
static
function
one
(
array
$elements
,
Closure
$validator
=
null
)
:
array
{
$rndElements
=
$elements
;
shuffle
(
$rndElements
);
foreach
(
self
::
getGenerator
(
$rndElements
)
as
$combination
)
{
$combination
=
array_combine
(
$elements
,
$combination
);
if
(
isset
(
$validator
)
&&
!
self
::
isCombinationValid
(
$combination
,
$validator
))
{
continue
;
}
return
$combination
;
}
return
[];
}
protected
static
function
getGenerator
(
array
$elements
)
:
Traversable
{
if
(
$elements
===
[])
{
return
;
}
foreach
(
self
::
getGenerator_internal
(
$elements
)
as
$combination
)
{
yield
$combination
;
}
}
private
static
function
getGenerator_internal
(
array
$elements
,
$combination
=
[])
:
Traversable
{
if
(
$elements
===
[])
{
yield
$combination
;
}
else
{
foreach
(
$elements
as
$idx
=>
$element
)
{
// Take the first element
// Add it to the combination
$tmpCombination
=
$combination
;
$tmpCombination
[]
=
$element
;
// And remove it from possibilities
$tmpElements
=
$elements
;
unset
(
$tmpElements
[
$idx
]);
yield
from
self
::
getGenerator_internal
(
$tmpElements
,
$tmpCombination
);
}
}
}
private
static
function
isCombinationValid
(
array
$combination
,
Closure
$validator
)
:
bool
{
foreach
(
$combination
as
$elementA
=>
$elementB
)
{
if
(
!
$validator
(
$elementA
,
$elementB
))
{
return
false
;
}
}
return
true
;
}
}
app/Libs/HatSolver.php
0 → 100644
View file @
9af41332
<?php
namespace
Korko\SecretSanta\Libs
;
use
Exception
;
use
Generator
;
class
HatSolver
{
public
function
one
(
array
$participants
,
array
$exclusions
=
[])
:
array
{
$hat
=
array_keys
(
$participants
);
shuffle
(
$hat
);
$generator
=
$this
->
getGenerator
(
$participants
,
$exclusions
,
$hat
);
if
(
!
$generator
->
valid
())
{
throw
new
Exception
(
'Cannot solve'
);
}
return
$generator
->
current
();
}
public
function
all
(
array
$participants
,
array
$exclusions
=
[])
:
array
{
$combinations
=
[];
$generator
=
$this
->
getGenerator
(
$participants
,
$exclusions
,
array_keys
(
$participants
));
foreach
(
$generator
as
$combination
)
{
$combinations
[]
=
$combination
;
}
return
$combinations
;
}
private
function
getGenerator
(
array
$participants
,
array
$exclusions
,
array
$hat
)
:
Generator
{
return
$this
->
_solve
(
0
,
[],
$exclusions
,
$hat
);
}
private
function
_solve
(
$i
,
array
$combination
,
array
$exclusions
,
array
$hat
)
:
Generator
{
$actualExclusions
=
array_key_exists
(
$i
,
$exclusions
)
?
$exclusions
[
$i
]
:
[];
$actualHat
=
array_diff
(
$hat
,
$actualExclusions
,
[
$i
]);
if
(
$actualHat
!==
[])
{
foreach
(
$actualHat
as
$possibility
)
{
$possibilityCombination
=
$combination
+
[
$i
=>
$possibility
];
$possibilityHat
=
array_diff
(
$hat
,
[
$possibility
]);
if
(
$possibilityHat
===
[])
{
yield
$possibilityCombination
;
}
else
{
yield
from
$this
->
_solve
(
$i
+
1
,
$possibilityCombination
,
$exclusions
,
$possibilityHat
);
}
}
}
else
{
return
;
}
}
}
app/Libs/Resolver.php
deleted
100644 → 0
View file @
f3ebd0af
<?php
namespace
Korko\SecretSanta\Libs
;
use
Exception
;
class
Resolver
{
public
static
function
resolve
(
array
$participants
)
:
array
{
$combination
=
Combinator
::
one
(
array_keys
(
$participants
),
function
(
$santa
,
$target
)
use
(
$participants
)
{
return
$santa
!==
$target
&&
!
in_array
(
$target
,
$participants
[
$santa
][
'exclusions'
]);
});
if
(
$combination
===
[])
{
throw
new
Exception
(
'Cannot resolve '
.
json_encode
(
$participants
));
}
foreach
(
$combination
as
$idx
=>
$idx2
)
{
$combination
[
$idx
]
=
$participants
[
$idx2
][
'name'
];
}
return
$combination
;
}
}
app/Providers/AppServiceProvider.php
View file @
9af41332
...
...
@@ -3,6 +3,7 @@
namespace
Korko\SecretSanta\Providers
;
use
Illuminate\Support\ServiceProvider
;
use
Korko\SecretSanta\Libs\HatSolver
as
LibHatSolver
;
use
Korko\SecretSanta\Libs\SmsTools
as
LibSmsTools
;
use
SmsTools
;
use
Validator
;
...
...
@@ -35,5 +36,8 @@ class AppServiceProvider extends ServiceProvider
$this
->
app
->
bind
(
'smstools'
,
function
(
$app
)
{
return
new
LibSmsTools
();
});
$this
->
app
->
bind
(
'solver'
,
function
(
$app
)
{
return
new
LibHatSolver
();
});
}
}
config/app.php
View file @
9af41332
...
...
@@ -206,6 +206,7 @@ return [
'Route'
=>
Illuminate\Support\Facades\Route
::
class
,
'Schema'
=>
Illuminate\Support\Facades\Schema
::
class
,
'Session'
=>
Illuminate\Support\Facades\Session
::
class
,
'Solver'
=>
Korko\SecretSanta\Facades\Solver
::
class
,
'Sms'
=>
Aloha\Twilio\Support\Laravel\Facade
::
class
,
'SmsTools'
=>
Korko\SecretSanta\Facades\SmsTools
::
class
,
'Statsd'
=>
League\StatsD\Laravel5\Facade\StatsdFacade
::
class
,
...
...
config/database.php
View file @
9af41332
...
...
@@ -48,7 +48,7 @@ return [
'sqlite'
=>
[
'driver'
=>
'sqlite'
,
'database'
=>
':memory:'
,
'database'
=>
storage_path
(
'databases/production.sqlite'
)
,
'prefix'
=>
''
,
],
...
...
storage/databases/production.sqlite
0 → 100644
View file @
9af41332
tests/ResolverTest.php
deleted
100644 → 0
View file @
f3ebd0af
<?php
use
Korko\SecretSanta\Libs\Resolver
;
class
ResolverTest
extends
TestCase
{
public
function
testDuoResolve
()
{
$this
->
assertEquals
([
0
=>
'B'
,
1
=>
'A'
],
Resolver
::
resolve
([
0
=>
[
'name'
=>
'A'
,
'exclusions'
=>
[]],
1
=>
[
'name'
=>
'B'
,
'exclusions'
=>
[]],
]));
$this
->
assertEquals
([
0
=>
'A'
,
1
=>
'B'
],
Resolver
::
resolve
([
0
=>
[
'name'
=>
'B'
,
'exclusions'
=>
[]],
1
=>
[
'name'
=>
'A'
,
'exclusions'
=>
[]],
]));
}
public
function
testTrioResolve
()
{
$this
->
assertEquals
([
0
=>
'B'
,
1
=>
'C'
,
2
=>
'A'
],
Resolver
::
resolve
([
0
=>
[
'name'
=>
'A'
,
'exclusions'
=>
[]],
1
=>
[
'name'
=>
'B'
,
'exclusions'
=>
[
'0'
]],
2
=>
[
'name'
=>
'C'
,
'exclusions'
=>
[
'1'
]],
]));
$this
->
assertEquals
([
0
=>
'C'
,
1
=>
'A'
,
2
=>
'B'
],
Resolver
::
resolve
([
0
=>
[
'name'
=>
'A'
,
'exclusions'
=>
[
'1'
]],
1
=>
[
'name'
=>
'B'
,
'exclusions'
=>
[
'2'
]],
2
=>
[
'name'
=>
'C'
,
'exclusions'
=>
[]],
]));
}
/**
* @expectedException Exception
* @expectedExceptionMessageRegExp #^Cannot resolve#
*/
public
function
testFailResolve
()
{
Resolver
::
resolve
([
0
=>
[
'name'
=>
'A'
,
'exclusions'
=>
[
'1'
,
'2'
]],
1
=>
[
'name'
=>
'B'
,
'exclusions'
=>
[]],
2
=>
[
'name'
=>
'C'
,
'exclusions'
=>
[]],
]);
}
}
tests/
Combinato
rTest.php
→
tests/
Solve
rTest.php
View file @
9af41332
<?php
use
Korko\SecretSanta\Libs\Combinator
;
class
CombinatorTest
extends
TestCase
class
SolverTest
extends
TestCase
{
public
function
assertCombination
(
$valid
,
$test
)
public
function
assertCombination
(
$valid
,
$test
,
$participants
)
{
$test
=
array_map
(
function
(
$el
)
{
$test
=
array_map
(
function
(
$el
)
use
(
$participants
)
{
array_walk
(
$el
,
function
(
&
$idx
)
use
(
$participants
)
{
$idx
=
$participants
[
$idx
];
});
return
implode
(
''
,
$el
);
},
$test
);
$this
->
assertEquals
(
sort
(
$valid
),
sort
(
$test
));
}
public
function
testNoFilter
()
{
$this
->
assertCombination
([
'AB'
,
'BA'
,
],
Combinator
::
all
([
'A'
,
'B'
]));
}
public
function
testNoExclusion
()
{
$this
->
assertCombination
([
'BA'
,
],
Combinator
::
all
([
'A'
,
'B'
],
function
(
$a
,
$b
)
{
return
$a
!==
$b
;
}));
],
Solver
::
all
([
'A'
,
'B'
]),
[
'A'
,
'B'
]);
$this
->
assertCombination
([
'BCA'
,
'CAB'
,
],
Combinator
::
all
([
'A'
,
'B'
,
'C'
],
function
(
$a
,
$b
)
{
return
$a
!==
$b
;
}));
],
Solver
::
all
([
'A'
,
'B'
,
'C'
]),
[
'A'
,
'B'
,
'C'
]);
$this
->
assertCombination
([
'BCDA'
,
'BDAC'
,
'BADC'
,
'CDAB'
,
'CADB'
,
'CDBA'
,
'DABC'
,
'DCBA'
,
'DCAB'
,
],
Combinator
::
all
([
'A'
,
'B'
,
'C'
,
'D'
],
function
(
$a
,
$b
)
{
return
$a
!==
$b
;
}));
],
Solver
::
all
([
'A'
,
'B'
,
'C'
,
'D'
]),
[
'A'
,
'B'
,
'C'
,
'D'
]);
}
public
function
testSimpleExclusion
()
{
$exclusions
=
[
'A'
=>
[
'C'
]];
// A => C
$this
->
assertCombination
([
'BCA'
,
],
Combinator
::
all
([
'A'
,
'B'
,
'C'
],
function
(
$elementA
,
$elementB
)
use
(
$exclusions
)
{
return
$elementA
!==
$elementB
&&
(
!
isset
(
$exclusions
[
$elementA
])
||
!
in_array
(
$elementB
,
$exclusions
[
$elementA
]));
}));
],
Solver
::
all
([
'A'
,
'B'
,
'C'
],
[
0
=>
[
2
]]),
[
'A'
,
'B'
,
'C'
]);
}
/*
public function testComplexExclusion()
{
$participants = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', L'];
$exclusions = [
'A'
=> [
'B', 'C', 'D'
]];
$exclusions = [
0
=> [
1, 2, 3
]];
$this->assertEquals(array(['A' => 'B', 'B' => 'C', 'C' => 'A']),
Combinato
r::all($participants, function($elementA, $elementB) use($exclusions) {
$this->assertEquals(array(['A' => 'B', 'B' => 'C', 'C' => 'A']),
Solve
r::all($participants, function($elementA, $elementB) use($exclusions) {
return !isset($exclusions[$elementA]) || !in_array($elementB, $exclusions[$elementA]);
}));
}
*/
public
function
testInvalidExclusion
()
public
function
testImpossibleSolution
()
{
$this
->
assertEquals
([],
Combinator
::
all
([
'A'
,
'B'
,
'C'
],
function
(
$elementA
,
$elementB
)
{
return
false
;
}));
$this
->
assertEquals
([],
Solver
::
all
([
'A'
,
'B'
,
'C'
],
[
0
=>
[
1
,
2
]]),
[
'A'
,
'B'
,
'C'
]);
}
public
function
test
ImpossibleSolution
()
public
function
test
One
()
{
$exclusions
=
[
'A'
=>
[
'B'
,
'C'
]];
srand
(
1
);
$this
->
assertEquals
([
0
=>
1
,
1
=>
2
,
2
=>
0
],
Solver
::
one
([
'A'
,
'B'
,
'C'
]));
$this
->
assertEquals
([],
Combinator
::
all
([
'A'
,
'B'
,
'C'
],
function
(
$elementA
,
$elementB
)
use
(
$exclusions
)
{
return
$elementA
!==
$elementB
&&
(
!
isset
(
$exclusions
[
$elementA
])
||
!
in_array
(
$elementB
,
$exclusions
[
$elementA
]));
}));
srand
(
123
);
$this
->
assertEquals
([
0
=>
2
,
1
=>
0
,
2
=>
1
],
Solver
::
one
([
'A'
,
'B'
,
'C'
]));
}
public
function
testMass
()
{
// 702 characters from 'A' to 'ZZ'
$participants
=
[];
for
(
$n
=
'A'
;
$n
!==
'AAA'
;
$n
++
)
{
$participants
[]
=
$n
;
}
Solver
::
one
(
$participants
);
}
}
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment